diff --git a/.buildkite/CUDA_Ext.yml b/.buildkite/CUDA_Ext.yml
index b5035f9fe..40a80d6e5 100644
--- a/.buildkite/CUDA_Ext.yml
+++ b/.buildkite/CUDA_Ext.yml
@@ -4,7 +4,7 @@ steps:
setup:
version:
- "1.10" # oldest
- #- "1" # latest
+ - "1" # latest
plugins:
- JuliaCI/julia#v1:
version: "{{matrix.version}}"
@@ -22,12 +22,3 @@ steps:
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 e8de06364..36199fd77 100644
--- a/.buildkite/pipeline.yml
+++ b/.buildkite/pipeline.yml
@@ -1,16 +1,25 @@
# see: https://github.com/staticfloat/forerunner-buildkite-plugin
steps:
- label: ":runner: Dynamically launch pipelines"
+ 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/)
+ agents:
+ queue: "juliagpu"
plugins:
- - staticfloat/forerunner:
+ - staticfloat/forerunner: # CUDA.jl tests
watch:
- ".buildkite/pipeline.yml"
- ".buildkite/CUDA_Ext.yml"
- "src/**"
- "ext/QuantumToolboxCUDAExt.jl"
- "test/runtests.jl"
- - "test/cuda_ext.jl"
+ - "test/ext-test/gpu/**"
- "Project.toml"
target: ".buildkite/CUDA_Ext.yml"
- agents:
- queue: "juliagpu"
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index ef9045062..e2f33b540 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -16,13 +16,13 @@ body:
label: Code to Reproduce the Bug
description: Please provide a minimal working example. Paste your code directly (It will be automatically formatted, so there's no need for backticks)
placeholder: "using QuantumToolbox\nprint(qeye(2))"
- render: shell
+ render: julia
- type: textarea
id: bug-output
attributes:
label: Code Output
description: Please paste the relevant output here (automatically formatted)
- placeholder: "Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true\n2×2 Diagonal{ComplexF64, Vector{ComplexF64}}:\n 1.0+0.0im ⋅ \n ⋅ 1.0+0.0im"
+ placeholder: "Quantum Object: type=Operator() dims=[2] size=(2, 2) ishermitian=true\n2×2 Diagonal{ComplexF64, Vector{ComplexF64}}:\n 1.0+0.0im ⋅ \n ⋅ 1.0+0.0im"
render: shell
- type: textarea
id: expected-behaviour
@@ -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: "Julia Ver. ***\nQuantumToolbox Ver. ***\nLinearSolve Ver. ***\nOrdinaryDiffEqCore Ver. ***\nOS : ***\nWORD_SIZE: ***\nLIBM : ***\nLLVM : ***\nBLAS : ***"
+ placeholder: "Julia Ver. ***\nQuantumToolbox Ver. ***\nSciMLOperators Ver. ***\nLinearSolve Ver. ***\nOrdinaryDiffEqCore Ver. ***\nOS : ***\nWORD_SIZE: ***\nLIBM : ***\nLLVM : ***\nBLAS : ***"
render: shell
validations:
required: true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ff6499d68..e0db79d1c 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,4 +4,8 @@ updates:
- package-ecosystem: "github-actions"
directory: "/" # Location of package manifests
schedule:
- interval: "weekly"
\ No newline at end of file
+ interval: "weekly"
+ labels:
+ - "dependencies"
+ - "Skip ChangeLog"
+
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index f486924a9..db12434e5 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,11 +1,12 @@
## Checklist
Thank you for contributing to `QuantumToolbox.jl`! Please make sure you have finished the following tasks before opening the PR.
-- [ ] 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.
+- [ ] Please read [Contributing to Quantum Toolbox in Julia](https://qutip.org/QuantumToolbox.jl/stable/resources/contributing).
+- [ ] Any code changes were done in a way that does not break public API.
+- [ ] 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`.
+- [ ] (If necessary) the `CHANGELOG.md` should be updated (regarding to the code changes) and built by running: `make changelog`.
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 +14,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/Benchmarks.yml b/.github/workflows/Benchmarks.yml
index e73652ebd..6b2247fdc 100644
--- a/.github/workflows/Benchmarks.yml
+++ b/.github/workflows/Benchmarks.yml
@@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: julia-actions/setup-julia@v2
with:
version: '1'
@@ -49,6 +49,7 @@ jobs:
Pkg.instantiate();
include("runbenchmarks.jl")'
+ # this will update benchmarks/data.js in gh-pages branch
- name: Parse & Upload Benchmark Results
uses: benchmark-action/github-action-benchmark@v1
with:
diff --git a/.github/workflows/CI-Julia-nightly.yml b/.github/workflows/CI-Julia-nightly.yml
new file mode 100644
index 000000000..79b02246f
--- /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'
+
+ steps:
+ - uses: actions/checkout@v5
+ - 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 }}
+ JULIA_NUM_THREADS: auto
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 0729432fd..119793312 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -8,7 +8,9 @@ on:
- '.github/workflows/CI.yml'
- 'src/**'
- 'ext/**'
- - 'test/**'
+ - 'test/runtests.jl'
+ - 'test/core-test/**'
+ - 'test/ext-test/cpu/**'
- 'Project.toml'
pull_request:
branches:
@@ -17,7 +19,9 @@ on:
- '.github/workflows/CI.yml'
- 'src/**'
- 'ext/**'
- - 'test/**'
+ - 'test/runtests.jl'
+ - 'test/core-test/**'
+ - 'test/ext-test/cpu/**'
- 'Project.toml'
types:
- opened
@@ -27,8 +31,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
@@ -39,53 +43,53 @@ jobs:
# for core tests (latest and oldest supported versions)
version:
- '1.10' # oldest
- # - '1' # latest
- os:
- - ubuntu-latest
- - windows-latest
- arch:
- - x64
+ - '1' # latest
+ node:
+ - os: 'ubuntu-latest'
+ arch: 'x64'
+ - os: 'windows-latest'
+ arch: 'x64'
+ - os: 'macOS-latest'
+ arch: 'arm64'
group:
- - Core
+ - 'Core'
include:
- # for core tests on macOS (M-series chip)
- - version: '1.10' # oldest
- os: 'macOS-latest'
- arch: 'x64'
- group: 'Core'
- # - version: '1' # latest
- # os: 'macOS-latest'
- # arch: 'arm64'
- # group: 'Core'
-
# for core tests (intermediate versions)
- # - version: '1.x'
- # os: 'ubuntu-latest'
- # arch: 'x64'
- # group: 'Core'
+ - version: '1.11'
+ node:
+ os: 'ubuntu-latest'
+ arch: 'x64'
+ group: 'Core'
- # for code quality tests
+ # for extension tests
- version: '1'
- os: 'ubuntu-latest'
- arch: 'x64'
- group: 'Code-Quality'
+ node:
+ os: 'ubuntu-latest'
+ arch: 'x64'
+ group: 'Makie_Ext'
+ - version: '1'
+ node:
+ os: 'ubuntu-latest'
+ arch: 'x64'
+ group: 'AutoDiff_Ext'
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- 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
env:
GROUP: ${{ matrix.group }}
+ JULIA_NUM_THREADS: auto
- uses: julia-actions/julia-processcoverage@v1
with:
directories: src,ext
- - uses: codecov/codecov-action@v4
+ - uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
diff --git a/.github/workflows/ChangeLogCheck.yml b/.github/workflows/ChangeLogCheck.yml
new file mode 100644
index 000000000..d99dca988
--- /dev/null
+++ b/.github/workflows/ChangeLogCheck.yml
@@ -0,0 +1,39 @@
+# Enforces the update of the file CHANGELOG.md on every pull request
+# Can be skipped with the `Skip ChangeLog` label
+name: ChangeLog Update Check
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
+
+jobs:
+ changelog:
+ runs-on: ubuntu-latest
+ if: ${{ !github.event.pull_request.draft }}
+ steps:
+ # check whether CHANGELOG.md is updated
+ - uses: dangoslen/changelog-enforcer@v3
+ with:
+ skipLabels: 'Skip ChangeLog'
+
+ # check whether the format of CHANGELOG.md is correct
+ - uses: actions/checkout@v5
+ - uses: julia-actions/setup-julia@v2
+ with:
+ version: '1'
+ - name: Install and Run Changelog
+ run: |
+ julia -e 'import Pkg; Pkg.add("Changelog")'
+ julia -e 'using Changelog; Changelog.generate(Changelog.CommonMark(), "CHANGELOG.md"; repo = "qutip/QuantumToolbox.jl")'
+
+ - name: CHANGELOG Format Check
+ run: |
+ julia -e '
+ output = Cmd(`git diff --name-only`) |> read |> String
+ if output == ""
+ exit(0)
+ else
+ @error "The format of CHANGELOG.md is not correct !!!"
+ write(stdout, "Please format it by running the following command:\n")
+ write(stdout, "make changelog")
+ exit(1)
+ end'
diff --git a/.github/workflows/CleanPreviewDoc.yml b/.github/workflows/CleanPreviewDoc.yml
new file mode 100644
index 000000000..dad91e5b5
--- /dev/null
+++ b/.github/workflows/CleanPreviewDoc.yml
@@ -0,0 +1,31 @@
+name: Cleanup Preview Documentation
+
+on:
+ pull_request:
+ types: [closed]
+
+permissions:
+ contents: write
+ deployments: write
+
+jobs:
+ cleanup-preview-doc:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout gh-pages branch
+ uses: actions/checkout@v5
+ with:
+ ref: gh-pages
+ - name: Delete preview and history + push changes
+ run: |
+ if [ -d "previews/PR$PRNUM" ]; then
+ git config user.name "Documenter.jl"
+ git config user.email "documenter@juliadocs.github.io"
+ git rm -rf "previews/PR$PRNUM"
+ git commit -m "delete preview"
+ git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree})
+ git push --force origin gh-pages-new:gh-pages
+ fi
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PRNUM: ${{ github.event.number }}
diff --git a/.github/workflows/Code-Quality.yml b/.github/workflows/Code-Quality.yml
new file mode 100644
index 000000000..8bd92249f
--- /dev/null
+++ b/.github/workflows/Code-Quality.yml
@@ -0,0 +1,61 @@
+name: Code Quality
+
+on:
+ push:
+ branches:
+ - 'main'
+ paths:
+ - '.github/workflows/Code-Quality.yml'
+ - 'src/**'
+ - 'ext/**'
+ - 'test/runtests.jl'
+ - 'test/core-test/**'
+ - 'Project.toml'
+ pull_request:
+ branches:
+ - 'main'
+ paths:
+ - '.github/workflows/Code-Quality.yml'
+ - 'src/**'
+ - 'ext/**'
+ - 'test/runtests.jl'
+ - 'test/core-test/**'
+ - 'Project.toml'
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - ready_for_review
+
+jobs:
+ test:
+ name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
+ 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:
+ - 'lts'
+ - '1'
+ os:
+ - 'ubuntu-latest'
+ arch:
+ - 'x64'
+ group:
+ - 'Code-Quality'
+
+ steps:
+ - uses: actions/checkout@v5
+ - 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/FormatCheck.yml b/.github/workflows/FormatCheck.yml
index 2b46352d5..e819e6a78 100644
--- a/.github/workflows/FormatCheck.yml
+++ b/.github/workflows/FormatCheck.yml
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: julia-actions/setup-julia@v2
with:
version: '1'
@@ -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/.github/workflows/SpellCheck.yml b/.github/workflows/SpellCheck.yml
new file mode 100644
index 000000000..9012b7532
--- /dev/null
+++ b/.github/workflows/SpellCheck.yml
@@ -0,0 +1,13 @@
+name: Spell Check
+
+on: [pull_request]
+
+jobs:
+ typos-check:
+ name: Spell Check with Typos
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Actions Repository
+ uses: actions/checkout@v5
+ - name: Check spelling
+ uses: crate-ci/typos@v1.37.2
\ No newline at end of file
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index beaa3fc02..d7ddeabbd 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -21,15 +21,29 @@ on:
- synchronize
- ready_for_review
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: write
+ pages: write
+ id-token: write
+ statuses: write
+
+# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
+# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
jobs:
+ # Build job
build:
runs-on: ubuntu-latest
- permissions:
- contents: write
- statuses: write
if: ${{ !github.event.pull_request.draft }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: julia-actions/setup-julia@v2
with:
version: '1'
@@ -37,11 +51,8 @@ jobs:
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-docdeploy@v1
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}
- - run: |
- julia --project=docs -e '
- using Documenter: DocMeta, doctest
- using QuantumToolbox
- DocMeta.setdocmeta!(QuantumToolbox, :DocTestSetup, :(using QuantumToolbox); recursive=true)
- doctest(QuantumToolbox)'
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token
+ DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key
+ JULIA_DEBUG: "Documenter"
+ DATADEPS_ALWAYS_ACCEPT: true
+ # GKSwstype: "100" # for Plots.jl plots (if you have them)
diff --git a/.gitignore b/.gitignore
index edb4c3a5e..b07886a94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,10 +4,10 @@
*.jl.cov
*.jl.mem
Manifest.toml
-docs/build/
.vscode
-*.json
+
+benchmarks/benchmarks_output.json
.ipynb_checkpoints
*.ipynb
\ No newline at end of file
diff --git a/.typos.toml b/.typos.toml
new file mode 100644
index 000000000..cb4a74b2f
--- /dev/null
+++ b/.typos.toml
@@ -0,0 +1,4 @@
+[default.extend-words]
+ket = "ket"
+sme = "sme"
+nd = "nd"
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..8a8eb735c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,330 @@
+# ChangeLog
+
+All notable changes to [`QuantumToolbox.jl`](https://github.com/qutip/QuantumToolbox.jl) will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased](https://github.com/qutip/QuantumToolbox.jl/tree/main)
+
+- Fix `cite()` bibtex output. ([#552])
+- Add `qeye_like` and `qzero_like`, which are synonyms of `one` and `zero`. ([#555])
+
+## [v0.36.0]
+Release date: 2025-09-29
+
+- Add `QuantumToolbox.cite()` for bibtex generator of `QuantumToolbox.jl`. ([#544])
+- Add `sortby` and `rev` keyword arguments to eigensolvers. ([#546])
+
+## [v0.35.0]
+Release date: 2025-09-03
+
+- Add support of `QobjEvo` for `steadystate` (ODE solver only). ([#536])
+- Changes to `SteadyStateODESolver`. ([#537])
+ - Introduce the tolerances for `steadystate` terminate condition (two new fields: `terminate_reltol = 1e-5` and `terminate_abstol = 1e-7`)
+ - Fix keyword argument handling for `SteadyStateODESolver` before passing to `mesolve`.
+- Fix incorrect `negativity` and `partial_transpose` for arbitrary subsystem dimension. ([#539])
+
+## [v0.34.1]
+Release date: 2025-08-23
+
+- Improve Bloch sphere rendering for animation. ([#520])
+- Add support to `Enzyme.jl` for `sesolve` and `mesolve`. ([#531])
+
+## [v0.34.0]
+Release date: 2025-07-29
+
+- Improve efficiency of `bloch_redfield_tensor` by avoiding unnecessary conversions. ([#509])
+- Support `SciMLOperators v1.4+`. ([#470])
+- Fix compatibility with `Makie v0.24+`. ([#513])
+- Add `keep_runs_results` option for multi-trajectory solvers to align with `QuTiP`. ([#512])
+ - Breaking changes for multi-trajectory solutions:
+ - the original fields `expect` and `states` now store the results depend on keyword argument `keep_runs_results` (decide whether to store all trajectories results or not).
+ - remove field `average_expect`
+ - remove field `runs_expect`
+ - New statistical analysis functions:
+ - `average_states`
+ - `average_expect`
+ - `std_expect`
+- Add support to ForwardDiff.jl for `sesolve` and `mesolve`. ([#515])
+- Add documentation about automatic differentiation. ([#517])
+
+## [v0.33.0]
+Release date: 2025-07-22
+
+- Implement `EnrSpace` and corresponding functionality. ([#500])
+- Check for orthogonality breakdown in `Lanczos` solver for `spectrum`. ([#501])
+- Store both `times` and `times_states` in time evolution solutions. ([#506], [#504])
+- Fix errors in `Julia v1.12`. ([#507])
+
+## [v0.32.1]
+Release date: 2025-06-24
+
+This is a release just for updating documentation.
+
+## [v0.32.0]
+Release date: 2025-06-23
+
+- Introduce `Lanczos` solver for `spectrum`. ([#476])
+- Add Bloch-Redfield master equation solver. ([#473])
+- Implement Bloch Sphere rendering and align style with qutip. ([#472], [#480], [#485], [#487], [#489])
+- Add `Base.copy` method for `AbstractQuantumObject`. ([#486])
+- Add documentation for Bloch-Redfield master equation. ([#494])
+
+## [v0.31.1]
+Release date: 2025-05-16
+
+- Introduce `QuantumToolbox.settings` and `auto_tidyup`. ([#460])
+
+## [v0.31.0]
+Release date: 2025-05-03
+
+- Return `sesolve` when `mesolve` allows it. ([#455])
+- Simplify structure of `QuantumObjectType`s. ([#456])
+
+## [v0.30.1]
+Release date: 2025-04-24
+
+- Support different length for `to` and `from` on GeneralDimensions. ([#448])
+- Extend the `Makie.jl` extension to all the other available backends. ([#450])
+- Fix definition of noise derivative in stochastic solvers. ([#453])
+
+## [v0.30.0]
+Release date: 2025-04-12
+
+- Make CUDA conversion more general using Adapt.jl. ([#436], [#437])
+- Make the generation of `fock` states non-mutating to support Zygote.jl. ([#438])
+- Remove Reexport.jl from the dependencies. ([#443])
+- Add support for automatic differentiation for `sesolve` and `mesolve`. ([#440])
+
+## [v0.29.1]
+Release date: 2025-03-07
+
+- Minor changes for GPU matrices element type and word size handling. ([#430])
+
+## [v0.29.0]
+Release date: 2025-03-07
+
+- Add support for `OperatorKet` state input for `mesolve` and `smesolve`. ([#423])
+- Introduce `plot_fock_distribution` to plot the population of a state (ket, bra, or density matrix) in its basis (assumed to be Fock basis). ([#428])
+
+## [v0.28.0]
+Release date: 2025-02-22
+
+- Support for single `AbstractQuantumObject` in `sc_ops` for faster specific method in `ssesolve` and `smesolve`. ([#408])
+- Change save callbacks from `PresetTimeCallback` to `FunctionCallingCallback`. ([#410])
+- Align `eigenstates` and `eigenenergies` to QuTiP. ([#411])
+- Introduce `vector_to_operator` and `operator_to_vector`. ([#413])
+- Introduce some entropy related functions. ([#414], [#416])
+ - `entropy_linear`
+ - `entropy_mutual`
+ - `entropy_conditional`
+ - `entropy_relative`
+- Fix `entanglement` and introduce `concurrence`. ([#414], [#418], [#419])
+- Introduce some metric functions. ([#414], [#420])
+ - `hilbert_dist`
+ - `hellinger_dist`
+ - `bures_dist`
+ - `bures_angle`
+- Align `steadystate` ODE solver to other methods and improve GPU support. ([#421])
+
+## [v0.27.0]
+Release date: 2025-02-14
+
+- Rename `sparse_to_dense` as `to_dense` and `dense_to_sparse` as `to_sparse`. ([#392])
+- Fix erroneous definition of the stochastic term in `smesolve`. ([#393])
+- Change name of `MultiSiteOperator` to `multisite_operator`. ([#394])
+- Fix `smesolve` for specifying initial state as density matrix. ([#395])
+- Add more generic solver for `steadystate_floquet` to allow more linear solvers. ([#396])
+- Fix time evolution output when using `saveat` keyword argument. ([#398])
+- Align some attributes of `mcsolve`, `ssesolve` and `smesolve` results with `QuTiP`. ([#402])
+- Improve ensemble generation of `ssesolve` and change parameters handling on stochastic processes. ([#403])
+- Set default trajectories to 500 and rename the keyword argument `ensemble_method` to `ensemblealg`. ([#405])
+- Introduce measurement on `ssesolve` and `smesolve`. ([#404])
+
+## [v0.26.0]
+Release date: 2025-02-09
+
+- Fix CUDA `sparse_to_dense`. ([#386])
+- Improve pseudo inverse spectrum solver. ([#388])
+- Add `smesolve` function for stochastic master equation. ([#389])
+
+## [v0.25.2]
+Release date: 2025-02-02
+
+- Move code quality dependencies to separate environment. ([#380])
+- Add additional normalization of the state during time evolution of `ssesolve`. This improves the numerical stability of the solver. ([#383])
+
+## [v0.25.1]
+Release date: 2025-01-29
+
+- Fix Dynamical Fock Dimension states saving due to wrong saving of dimensions. ([#375])
+- Support a list of observables for `expect`. ([#374], [#376])
+- Add checks for `tlist` in time evolution solvers. The checks are to ensure that `tlist` is not empty, the elements are in increasing order, and the elements are unique. ([#378])
+
+## [v0.25.0]
+Release date: 2025-01-20
+
+- Change the structure of block diagonalization functions, using `BlockDiagonalForm` struct and changing the function name from `bdf` to `block_diagonal_form`. ([#349])
+- Add **GPUArrays** compatibility for `ptrace` function, by using **KernelAbstractions.jl**. ([#350])
+- Introduce `Space`, `Dimensions`, `GeneralDimensions` structures to support wider definitions and operations of `Qobj/QobjEvo`, and potential functionalities in the future. ([#271], [#353], [#360])
+- Improve lazy tensor warning for `SciMLOperators`. ([#370])
+- Change order of `AbstractQuantumObject` data type. For example, from `QuantumObject{DataType,ObjType,DimsType}` to `QuantumObject{ObjType,DimsType,DataType}`. ([#371])
+
+## [v0.24.0]
+Release date: 2024-12-13
+
+- Improve the construction of `QobjEvo`. ([#338], [#339])
+- Support `Base.zero` and `Base.one` for `AbstractQuantumObject`. ([#342], [#346])
+- Introduce visualization and function `plot_wigner` for easy plotting of Wigner functions. ([#86], [#292], [#347])
+
+## [v0.23.1]
+Release date: 2024-12-06
+
+- Update `[compat]` to fix the incompatibility between `QuantumToolbox v0.22.0+` and `DiffEqCallbacks < v4.2.1`. ([#335])
+
+## [v0.23.0]
+Release date: 2024-12-04
+
+- Change `SingleSiteOperator` with the more general `MultiSiteOperator`. ([#324])
+- Make `spectrum` and `correlation` functions align with `Python QuTiP`, introduce spectrum solver `PseudoInverse`, remove spectrum solver `FFTCorrelation`, and introduce `spectrum_correlation_fft`. ([#330])
+
+## [v0.22.0]
+Release date: 2024-11-20
+
+- Change the parameters structure of `sesolve`, `mesolve` and `mcsolve` functions to possibly support automatic differentiation. ([#311])
+- Fix type instability and reduce extra memory allocation in `liouvillian`. ([#315], [#318])
+
+## [v0.21.5]
+Release date: 2024-11-15
+
+- This is a demonstration of how to bump version number and also modify `CHANGELOG.md` before new release. ([#309])
+
+## [v0.21.4]
+Release date: 2024-11-13
+
+- This is just a demonstration about [`Changelog.jl`](https://github.com/JuliaDocs/Changelog.jl). ([#139], [#306])
+
+
+
+
+[v0.21.4]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.21.4
+[v0.21.5]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.21.5
+[v0.22.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.22.0
+[v0.23.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.23.0
+[v0.23.1]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.23.1
+[v0.24.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.24.0
+[v0.25.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.25.0
+[v0.25.1]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.25.1
+[v0.25.2]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.25.2
+[v0.26.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.26.0
+[v0.27.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.27.0
+[v0.28.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.28.0
+[v0.29.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.29.0
+[v0.29.1]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.29.1
+[v0.30.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.30.0
+[v0.30.1]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.30.1
+[v0.31.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.31.0
+[v0.31.1]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.31.1
+[v0.32.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.32.0
+[v0.32.1]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.32.1
+[v0.33.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.33.0
+[v0.34.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.34.0
+[v0.34.1]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.34.1
+[v0.35.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.35.0
+[v0.36.0]: https://github.com/qutip/QuantumToolbox.jl/releases/tag/v0.36.0
+[#86]: https://github.com/qutip/QuantumToolbox.jl/issues/86
+[#139]: https://github.com/qutip/QuantumToolbox.jl/issues/139
+[#271]: https://github.com/qutip/QuantumToolbox.jl/issues/271
+[#292]: https://github.com/qutip/QuantumToolbox.jl/issues/292
+[#306]: https://github.com/qutip/QuantumToolbox.jl/issues/306
+[#309]: https://github.com/qutip/QuantumToolbox.jl/issues/309
+[#311]: https://github.com/qutip/QuantumToolbox.jl/issues/311
+[#315]: https://github.com/qutip/QuantumToolbox.jl/issues/315
+[#318]: https://github.com/qutip/QuantumToolbox.jl/issues/318
+[#324]: https://github.com/qutip/QuantumToolbox.jl/issues/324
+[#330]: https://github.com/qutip/QuantumToolbox.jl/issues/330
+[#335]: https://github.com/qutip/QuantumToolbox.jl/issues/335
+[#338]: https://github.com/qutip/QuantumToolbox.jl/issues/338
+[#339]: https://github.com/qutip/QuantumToolbox.jl/issues/339
+[#342]: https://github.com/qutip/QuantumToolbox.jl/issues/342
+[#346]: https://github.com/qutip/QuantumToolbox.jl/issues/346
+[#347]: https://github.com/qutip/QuantumToolbox.jl/issues/347
+[#349]: https://github.com/qutip/QuantumToolbox.jl/issues/349
+[#350]: https://github.com/qutip/QuantumToolbox.jl/issues/350
+[#353]: https://github.com/qutip/QuantumToolbox.jl/issues/353
+[#360]: https://github.com/qutip/QuantumToolbox.jl/issues/360
+[#370]: https://github.com/qutip/QuantumToolbox.jl/issues/370
+[#371]: https://github.com/qutip/QuantumToolbox.jl/issues/371
+[#374]: https://github.com/qutip/QuantumToolbox.jl/issues/374
+[#375]: https://github.com/qutip/QuantumToolbox.jl/issues/375
+[#376]: https://github.com/qutip/QuantumToolbox.jl/issues/376
+[#378]: https://github.com/qutip/QuantumToolbox.jl/issues/378
+[#380]: https://github.com/qutip/QuantumToolbox.jl/issues/380
+[#383]: https://github.com/qutip/QuantumToolbox.jl/issues/383
+[#386]: https://github.com/qutip/QuantumToolbox.jl/issues/386
+[#388]: https://github.com/qutip/QuantumToolbox.jl/issues/388
+[#389]: https://github.com/qutip/QuantumToolbox.jl/issues/389
+[#392]: https://github.com/qutip/QuantumToolbox.jl/issues/392
+[#393]: https://github.com/qutip/QuantumToolbox.jl/issues/393
+[#394]: https://github.com/qutip/QuantumToolbox.jl/issues/394
+[#395]: https://github.com/qutip/QuantumToolbox.jl/issues/395
+[#396]: https://github.com/qutip/QuantumToolbox.jl/issues/396
+[#398]: https://github.com/qutip/QuantumToolbox.jl/issues/398
+[#402]: https://github.com/qutip/QuantumToolbox.jl/issues/402
+[#403]: https://github.com/qutip/QuantumToolbox.jl/issues/403
+[#404]: https://github.com/qutip/QuantumToolbox.jl/issues/404
+[#405]: https://github.com/qutip/QuantumToolbox.jl/issues/405
+[#408]: https://github.com/qutip/QuantumToolbox.jl/issues/408
+[#410]: https://github.com/qutip/QuantumToolbox.jl/issues/410
+[#411]: https://github.com/qutip/QuantumToolbox.jl/issues/411
+[#413]: https://github.com/qutip/QuantumToolbox.jl/issues/413
+[#414]: https://github.com/qutip/QuantumToolbox.jl/issues/414
+[#416]: https://github.com/qutip/QuantumToolbox.jl/issues/416
+[#418]: https://github.com/qutip/QuantumToolbox.jl/issues/418
+[#419]: https://github.com/qutip/QuantumToolbox.jl/issues/419
+[#420]: https://github.com/qutip/QuantumToolbox.jl/issues/420
+[#421]: https://github.com/qutip/QuantumToolbox.jl/issues/421
+[#423]: https://github.com/qutip/QuantumToolbox.jl/issues/423
+[#428]: https://github.com/qutip/QuantumToolbox.jl/issues/428
+[#430]: https://github.com/qutip/QuantumToolbox.jl/issues/430
+[#436]: https://github.com/qutip/QuantumToolbox.jl/issues/436
+[#437]: https://github.com/qutip/QuantumToolbox.jl/issues/437
+[#438]: https://github.com/qutip/QuantumToolbox.jl/issues/438
+[#440]: https://github.com/qutip/QuantumToolbox.jl/issues/440
+[#443]: https://github.com/qutip/QuantumToolbox.jl/issues/443
+[#448]: https://github.com/qutip/QuantumToolbox.jl/issues/448
+[#450]: https://github.com/qutip/QuantumToolbox.jl/issues/450
+[#453]: https://github.com/qutip/QuantumToolbox.jl/issues/453
+[#455]: https://github.com/qutip/QuantumToolbox.jl/issues/455
+[#456]: https://github.com/qutip/QuantumToolbox.jl/issues/456
+[#460]: https://github.com/qutip/QuantumToolbox.jl/issues/460
+[#470]: https://github.com/qutip/QuantumToolbox.jl/issues/470
+[#472]: https://github.com/qutip/QuantumToolbox.jl/issues/472
+[#473]: https://github.com/qutip/QuantumToolbox.jl/issues/473
+[#476]: https://github.com/qutip/QuantumToolbox.jl/issues/476
+[#480]: https://github.com/qutip/QuantumToolbox.jl/issues/480
+[#485]: https://github.com/qutip/QuantumToolbox.jl/issues/485
+[#486]: https://github.com/qutip/QuantumToolbox.jl/issues/486
+[#487]: https://github.com/qutip/QuantumToolbox.jl/issues/487
+[#489]: https://github.com/qutip/QuantumToolbox.jl/issues/489
+[#494]: https://github.com/qutip/QuantumToolbox.jl/issues/494
+[#500]: https://github.com/qutip/QuantumToolbox.jl/issues/500
+[#501]: https://github.com/qutip/QuantumToolbox.jl/issues/501
+[#504]: https://github.com/qutip/QuantumToolbox.jl/issues/504
+[#506]: https://github.com/qutip/QuantumToolbox.jl/issues/506
+[#507]: https://github.com/qutip/QuantumToolbox.jl/issues/507
+[#509]: https://github.com/qutip/QuantumToolbox.jl/issues/509
+[#512]: https://github.com/qutip/QuantumToolbox.jl/issues/512
+[#513]: https://github.com/qutip/QuantumToolbox.jl/issues/513
+[#515]: https://github.com/qutip/QuantumToolbox.jl/issues/515
+[#517]: https://github.com/qutip/QuantumToolbox.jl/issues/517
+[#520]: https://github.com/qutip/QuantumToolbox.jl/issues/520
+[#531]: https://github.com/qutip/QuantumToolbox.jl/issues/531
+[#536]: https://github.com/qutip/QuantumToolbox.jl/issues/536
+[#537]: https://github.com/qutip/QuantumToolbox.jl/issues/537
+[#539]: https://github.com/qutip/QuantumToolbox.jl/issues/539
+[#544]: https://github.com/qutip/QuantumToolbox.jl/issues/544
+[#546]: https://github.com/qutip/QuantumToolbox.jl/issues/546
+[#552]: https://github.com/qutip/QuantumToolbox.jl/issues/552
+[#555]: https://github.com/qutip/QuantumToolbox.jl/issues/555
diff --git a/CITATION.bib b/CITATION.bib
new file mode 100644
index 000000000..265f6af66
--- /dev/null
+++ b/CITATION.bib
@@ -0,0 +1,13 @@
+@article{QuantumToolbox.jl2025,
+ title = {Quantum{T}oolbox.jl: {A}n efficient {J}ulia framework for simulating open quantum systems},
+ author = {Mercurio, Alberto and Huang, Yi-Te and Cai, Li-Xun and Chen, Yueh-Nan and Savona, Vincenzo and Nori, Franco},
+ journal = {{Quantum}},
+ issn = {2521-327X},
+ publisher = {{Verein zur F{\"{o}}rderung des Open Access Publizierens in den Quantenwissenschaften}},
+ volume = {9},
+ pages = {1866},
+ month = sep,
+ year = {2025},
+ doi = {10.22331/q-2025-09-29-1866},
+ url = {https://doi.org/10.22331/q-2025-09-29-1866}
+}
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..60a1f8050
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+JULIA:=julia
+
+default: help
+
+setup:
+ ${JULIA} -e 'import Pkg; Pkg.add(["JuliaFormatter", "Changelog"])'
+
+format:
+ ${JULIA} -e 'using JuliaFormatter; format(".")'
+
+changelog:
+ ${JULIA} -e 'using Changelog; Changelog.generate(Changelog.CommonMark(), "CHANGELOG.md"; repo = "qutip/QuantumToolbox.jl")'
+
+test:
+ ${JULIA} --project -e 'using Pkg; Pkg.update(); Pkg.test()'
+
+docs:
+ ${JULIA} --project=docs -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.update()'
+ ${JULIA} --project=docs docs/make.jl
+
+vitepress:
+ npm --prefix docs run docs:dev
+
+all: setup format changelog test docs vitepress
+
+help:
+ @echo "The following make commands are available:"
+ @echo " - make setup: install the dependencies for make command"
+ @echo " - make format: format codes with JuliaFormatter"
+ @echo " - make changelog: generate changelog"
+ @echo " - make test: run the tests"
+ @echo " - make docs: instantiate and build the documentation"
+ @echo " - make vitepress: start Vitepress site of documentation"
+ @echo " - make all: run every commands in the above order"
+
+.PHONY: default setup format changelog test docs vitepress all help
diff --git a/Project.toml b/Project.toml
index 63533afb2..142d7bf27 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,60 +1,71 @@
name = "QuantumToolbox"
uuid = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab"
-authors = ["Alberto Mercurio", "Luca Gravina", "Yi-Te Huang"]
-version = "0.13.1"
+authors = ["Alberto Mercurio", "Yi-Te Huang"]
+version = "0.36.0"
[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"
+Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
IncompleteLU = "40713840-3770-5561-ab4c-a76e7d0d7895"
+LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae"
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"
+Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
+StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
[weakdeps]
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
+ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7"
+KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
+Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
[extensions]
QuantumToolboxCUDAExt = "CUDA"
+QuantumToolboxChainRulesCoreExt = "ChainRulesCore"
+QuantumToolboxGPUArraysExt = ["GPUArrays", "KernelAbstractions"]
+QuantumToolboxMakieExt = "Makie"
[compat]
ArrayInterface = "6, 7"
CUDA = "5"
+ChainRulesCore = "1"
DiffEqBase = "6"
-DiffEqCallbacks = "2 - 3.1, 3.8"
+DiffEqCallbacks = "4.2.1 - 4"
+DiffEqNoiseProcess = "5"
+Distributed = "1"
FFTW = "1.5"
+GPUArrays = "10, 11"
Graphs = "1.7"
IncompleteLU = "0.2"
-LinearAlgebra = "<0.0.1, 1"
-LinearSolve = "2"
+KernelAbstractions = "0.9.2"
+LaTeXStrings = "1.2"
+LinearAlgebra = "1"
+LinearSolve = "2, 3"
+Makie = "0.24"
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"
+Pkg = "1"
+Random = "1"
+SciMLBase = "2.105"
+SciMLOperators = "1.4"
+SparseArrays = "1"
SpecialFunctions = "2"
StaticArraysCore = "1"
-Test = "<0.0.1, 1"
+Statistics = "1"
+StochasticDiffEq = "6"
julia = "1.10"
-
-[extras]
-CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
-Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
-
-[targets]
-test = ["Test"]
diff --git a/README.md b/README.md
index 369f9cec2..c9935660a 100644
--- a/README.md
+++ b/README.md
@@ -6,14 +6,14 @@
# QuantumToolbox.jl
-[A. Mercurio](https://github.com/albertomercurio),
-[L. Gravina](https://github.com/lgravina1997),
+[A. Mercurio](https://github.com/albertomercurio)
and [Y.-T. Huang](https://github.com/ytdHuang).
-| **Release** | [![Release][release-img]][release-url] [![License][license-img]][license-url] [![DOI][doi-img]][doi-url] [![Downloads][download-img]][download-url] |
+| **Release** | [![Release][release-img]][release-url] [![License][license-img]][license-url] [![Cite][cite-img]][cite-url] [![Downloads][download-img]][download-url] |
|:-----------------:|:-------------|
-| **Runtests** | [![Runtests][runtests-img]][runtests-url] [![Coverage][codecov-img]][codecov-url] [![Aqua QA][aqua-img]][aqua-url] [![JET][jet-img]][jet-url] |
+| **Runtests** | [![Runtests][runtests-img]][runtests-url] [![Coverage][codecov-img]][codecov-url] |
+| **Code Quality** | [![Code Quality][code-quality-img]][code-quality-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** | [](https://unitary.fund) |
@@ -24,8 +24,8 @@ and [Y.-T. Huang](https://github.com/ytdHuang).
[license-img]: https://img.shields.io/badge/license-New%20BSD-blue.svg
[license-url]: https://opensource.org/licenses/BSD-3-Clause
-[doi-img]: https://zenodo.org/badge/DOI/10.5281/zenodo.10822816.svg
-[doi-url]: https://doi.org/10.5281/zenodo.10822816
+[cite-img]: https://img.shields.io/badge/cite-Quantum_9%2C_1866_(2025)-blue
+[cite-url]: https://doi.org/10.22331/q-2025-09-29-1866
[download-img]: https://img.shields.io/badge/dynamic/json?url=http%3A%2F%2Fjuliapkgstats.com%2Fapi%2Fv1%2Ftotal_downloads%2FQuantumToolbox&query=total_requests&label=Downloads
[download-url]: https://juliapkgstats.com/pkg/QuantumToolbox
@@ -36,6 +36,9 @@ and [Y.-T. Huang](https://github.com/ytdHuang).
[codecov-img]: https://codecov.io/gh/qutip/QuantumToolbox.jl/branch/main/graph/badge.svg
[codecov-url]: https://codecov.io/gh/qutip/QuantumToolbox.jl
+[code-quality-img]: https://github.com/qutip/QuantumToolbox.jl/actions/workflows/Code-Quality.yml/badge.svg
+[code-quality-url]: https://github.com/qutip/QuantumToolbox.jl/actions/workflows/Code-Quality.yml
+
[aqua-img]: https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg
[aqua-url]: https://github.com/JuliaTesting/Aqua.jl
@@ -52,30 +55,32 @@ and [Y.-T. Huang](https://github.com/ytdHuang).
## Introduction
-[QuantumToolbox.jl](https://github.com/qutip/QuantumToolbox.jl) is a cutting-edge Julia package designed for quantum physics simulations, closely emulating the popular Python [QuTiP](https://github.com/qutip/qutip) package. It uniquely combines the simplicity and power of Julia with advanced features like GPU acceleration and distributed computing, making simulation of quantum systems more accessible and efficient.
+[QuantumToolbox.jl](https://github.com/qutip/QuantumToolbox.jl) is a cutting-edge [`Julia`](https://julialang.org/) package designed for quantum physics simulations, closely emulating the popular Python [`QuTiP`](https://github.com/qutip/qutip) package. It uniquely combines the simplicity and power of [`Julia`](https://julialang.org/) with advanced features like GPU acceleration and distributed computing, making simulation of quantum systems more accessible and efficient.
*With this package, moving from Python to Julia for quantum physics simulations has never been easier*, due to the similar syntax and functionalities.
## Features
-QuantumToolbox.jl is equipped with a robust set of features:
+`QuantumToolbox.jl` is equipped with a robust set of features:
-- **Quantum State and Operator Manipulation:** Easily handle quantum states and operators with a rich set of tools, with the same functionalities as QuTiP.
-- **Dynamical Evolution:** Advanced solvers for time evolution of quantum systems, thanks to the powerful [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl) package.
-- **GPU Computing:** Leverage GPU resources for high-performance computing. For example, you run the master equation direclty on the GPU with the same syntax as the CPU case.
-- **Distributed Computing:** Distribute the computation over multiple nodes (e.g., a cluster). For example, you can run hundreds of quantum trajectories in parallel on a cluster, with, again, the same syntax as the simple case.
-- **Easy Extension:** Easily extend the package, taking advantage of the Julia language features, like multiple dispatch and metaprogramming.
+- **Quantum State and Operator Manipulation:** Easily handle quantum states and operators with a rich set of tools, with the same functionalities as `QuTiP`.
+- **Dynamical Evolution:** Advanced solvers for time evolution of quantum systems, thanks to the powerful [`DifferentialEquations.jl`](https://github.com/SciML/DifferentialEquations.jl) package.
+- **GPU Computing:** Leverage GPU resources for high-performance computing. Simulate quantum dynamics directly on the GPU with the same syntax as the CPU case.
+- **Distributed Computing:** Distribute the computation over multiple nodes (e.g., a cluster). For example, you can run hundreds of quantum trajectories in parallel on a cluster, with, again, the same syntax as the simple case. See [here](https://qutip.org/QuantumToolbox.jl/stable/users_guide/cluster) for more information.
+- **Differentiable Programming:** Enable gradient-based optimization for quantum algorithms. Compute gradients of quantum dynamics with respect to their parameters using automatic differentiation. See [here](https://qutip.org/QuantumToolbox.jl/stable/users_guide/autodiff) for more information.
+- **Easy Extension:** Easily extend the package, taking advantage of the `Julia` language features, like multiple dispatch and metaprogramming.
## Installation
-> **_NOTE:_** `QuantumToolbox.jl` requires `Julia 1.10+`.
+> [!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
using Pkg
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:
+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.10) pkg> add QuantumToolbox
```
@@ -90,7 +95,7 @@ QuantumToolbox.about()
## Brief Example
-We now provide a brief example to demonstrate the similarity between [QuantumToolbox.jl](https://github.com/qutip/QuantumToolbox.jl) and [QuTiP](https://github.com/qutip/qutip).
+We now provide a brief example to demonstrate the similarity between [`QuantumToolbox.jl`](https://github.com/qutip/QuantumToolbox.jl) and [`QuTiP`](https://github.com/qutip/qutip).
Let's consider a quantum harmonic oscillator with a Hamiltonian given by:
@@ -144,8 +149,6 @@ 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
@@ -162,3 +165,56 @@ e_ops = [a_gpu' * a_gpu]
sol = mesolve(H_gpu, ψ0_gpu, tlist, c_ops, e_ops = e_ops)
```
+
+## Performance comparison with other packages
+
+Here we provide a brief performance comparison between `QuantumToolbox.jl` and other popular quantum physics simulation packages, such as [`QuTiP`](https://github.com/qutip/qutip) (Python), [`dynamiqs`](https://github.com/dynamiqs/dynamiqs) (Python - JAX) and [`QuantumOptics.jl`](https://github.com/qojulia/QuantumOptics.jl) (Julia). We clearly show that `QuantumToolbox.jl` is the fastest package among the four. A detailed code is available [here](https://github.com/albertomercurio/QuantumToolbox.jl-Paper-Figures/blob/main/src/benchmarks/benchmarks.jl).
+
+
+
+## Contributing to QuantumToolbox.jl
+
+You are most welcome to contribute to `QuantumToolbox.jl` development by forking this repository and sending pull requests (PRs), or filing bug reports at the issues page. You can also help out with users' questions, or discuss proposed changes in the [QuTiP discussion group](https://groups.google.com/g/qutip).
+
+For more information about contribution, including technical advice, please see the [Contributing to Quantum Toolbox in Julia](https://qutip.org/QuantumToolbox.jl/stable/resources/contributing).
+
+## Cite `QuantumToolbox.jl`
+If you like `QuantumToolbox.jl`, we would appreciate it if you starred the repository in order to help us increase its visibility. Furthermore, if you find the framework useful in your research, we would be grateful if you could cite our publication [ [Quantum 9, 1866 (2025)](https://doi.org/10.22331/q-2025-09-29-1866) ] using the following bibtex entry:
+
+```bib
+@article{QuantumToolbox.jl2025,
+ title = {Quantum{T}oolbox.jl: {A}n efficient {J}ulia framework for simulating open quantum systems},
+ author = {Mercurio, Alberto and Huang, Yi-Te and Cai, Li-Xun and Chen, Yueh-Nan and Savona, Vincenzo and Nori, Franco},
+ journal = {{Quantum}},
+ issn = {2521-327X},
+ publisher = {{Verein zur F{\"{o}}rderung des Open Access Publizierens in den Quantenwissenschaften}},
+ volume = {9},
+ pages = {1866},
+ month = sep,
+ year = {2025},
+ doi = {10.22331/q-2025-09-29-1866},
+ url = {https://doi.org/10.22331/q-2025-09-29-1866}
+}
+```
+
+## Acknowledgements
+
+### Fundings
+
+`QuantumToolbox.jl` is supported by the [Unitary Fund](https://unitary.fund), a grant program for quantum technology projects.
+
+
+
+### Other Acknowledgements
+
+We are also grateful to the [Zulip](https://zulip.com) team for providing a free chat service for open-source projects.
+
+
diff --git a/benchmarks/correlations_and_spectrum.jl b/benchmarks/correlations_and_spectrum.jl
index a5eab65a8..d3ae9e916 100644
--- a/benchmarks/correlations_and_spectrum.jl
+++ b/benchmarks/correlations_and_spectrum.jl
@@ -1,3 +1,9 @@
+function _calculate_fft_spectrum(H, tlist, c_ops, A, B)
+ corr = correlation_2op_1t(H, nothing, tlist, c_ops, A, B; progress_bar = Val(false))
+ ωlist, spec = spectrum_correlation_fft(tlist, corr)
+ return nothing
+end
+
function benchmark_correlations_and_spectrum!(SUITE)
N = 15
ω = 1
@@ -9,11 +15,23 @@ function benchmark_correlations_and_spectrum!(SUITE)
c_ops = [sqrt(γ * (nth + 1)) * a, sqrt(γ * nth) * a']
ω_l = range(0, 3, length = 1000)
+ t_l = range(0, 333 * π, length = 1000)
+
+ PI_solver = PseudoInverse()
+
+ L_solver = Lanczos()
SUITE["Correlations and Spectrum"]["FFT Correlation"] =
- @benchmarkable spectrum($H, $ω_l, $(a'), $a, $c_ops, solver = FFTCorrelation(), progress_bar = false)
+ @benchmarkable _calculate_fft_spectrum($H, $t_l, $c_ops, $(a'), $a)
+
+ SUITE["Correlations and Spectrum"]["Spectrum"]["Exponential Series"] =
+ @benchmarkable spectrum($H, $ω_l, $c_ops, $(a'), $a)
+
+ SUITE["Correlations and Spectrum"]["Spectrum"]["Pseudo Inverse"] =
+ @benchmarkable spectrum($H, $ω_l, $c_ops, $(a'), $a, solver = $PI_solver)
- SUITE["Correlations and Spectrum"]["Exponential Series"] = @benchmarkable spectrum($H, $ω_l, $(a'), $a, $c_ops)
+ SUITE["Correlations and Spectrum"]["Spectrum"]["Lanczos"] =
+ @benchmarkable spectrum($H, $ω_l, $c_ops, $(a'), $a, solver = $L_solver)
return nothing
end
diff --git a/benchmarks/eigenvalues.jl b/benchmarks/eigenvalues.jl
index d6143b9a2..3fab5302d 100644
--- a/benchmarks/eigenvalues.jl
+++ b/benchmarks/eigenvalues.jl
@@ -9,14 +9,15 @@ function benchmark_eigenvalues!(SUITE)
ωb = 1
g = 0.2
κ = 0.01
- n_thermal = 0.1
+ n_th = 0.1
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]
+ c_ops = [√((1 + n_th) * κ) * a, √κ * b, √(n_th * κ) * a_d]
L = liouvillian(H, c_ops)
SUITE["Eigenvalues"]["eigenstates"]["dense"] = @benchmarkable eigenstates($L)
- SUITE["Eigenvalues"]["eigenstates"]["sparse"] = @benchmarkable eigenstates($L, sparse = true, sigma = 0.01, k = 5)
+ SUITE["Eigenvalues"]["eigenstates"]["sparse"] =
+ @benchmarkable eigenstates($L, sparse = true, sigma = 0.01, eigvals = 5)
return nothing
end
diff --git a/benchmarks/runbenchmarks.jl b/benchmarks/runbenchmarks.jl
index e921c81a4..a8419c516 100644
--- a/benchmarks/runbenchmarks.jl
+++ b/benchmarks/runbenchmarks.jl
@@ -1,4 +1,6 @@
using BenchmarkTools
+using LinearAlgebra
+using SparseArrays
using QuantumToolbox
using OrdinaryDiffEq
using LinearSolve
diff --git a/benchmarks/timeevolution.jl b/benchmarks/timeevolution.jl
index da9919dc3..189c7beaf 100644
--- a/benchmarks/timeevolution.jl
+++ b/benchmarks/timeevolution.jl
@@ -47,20 +47,110 @@ function benchmark_timeevolution!(SUITE)
$ψ0,
$tlist,
$c_ops,
- n_traj = 100,
+ ntraj = 100,
e_ops = $e_ops,
progress_bar = Val(false),
- ensemble_method = EnsembleSerial(),
+ ensemblealg = EnsembleSerial(),
)
SUITE["Time Evolution"]["time-independent"]["mcsolve"]["Multithreaded"] = @benchmarkable mcsolve(
$H,
$ψ0,
$tlist,
$c_ops,
- n_traj = 100,
+ ntraj = 100,
e_ops = $e_ops,
progress_bar = Val(false),
- ensemble_method = EnsembleThreads(),
+ ensemblealg = EnsembleThreads(),
+ )
+
+ ## Time-dependent evolutions ##
+
+ # Hamiltonian in the lab frame (without drive frame transformation)
+ H_lab = ωc * a' * a + ωq / 2 * σz + g * (a' * σm + a * σm')
+
+ # Define time-dependent drive terms
+ coef1(p, t) = p.F * exp(1im * p.ωd * t)
+ coef2(p, t) = p.F * exp(-1im * p.ωd * t)
+ p = (F = F, ωd = ωd)
+
+ # Time-dependent Hamiltonian as tuple (lab frame with drive)
+ H_td = (H_lab, (a, coef1), (a', coef2))
+
+ # Time-dependent Hamiltonian as QobjEvo
+ H_td2 = QobjEvo(H_td)
+
+ # Time-dependent Liouvillian
+ L_td = liouvillian(H_td2)
+
+ tlist_td = range(0, 10 / γ, 100)
+
+ ## sesolve (time-dependent) ##
+
+ SUITE["Time Evolution"]["time-dependent"]["sesolve"]["Tuple"] =
+ @benchmarkable sesolve($H_td, $ψ0, $tlist_td, e_ops = $e_ops, progress_bar = Val(false), params = $p)
+
+ SUITE["Time Evolution"]["time-dependent"]["sesolve"]["QobjEvo"] =
+ @benchmarkable sesolve($H_td2, $ψ0, $tlist_td, e_ops = $e_ops, progress_bar = Val(false), params = $p)
+
+ ## mesolve (time-dependent) ##
+
+ SUITE["Time Evolution"]["time-dependent"]["mesolve"]["Tuple"] =
+ @benchmarkable mesolve($H_td, $ψ0, $tlist_td, $c_ops, e_ops = $e_ops, progress_bar = Val(false), params = $p)
+
+ SUITE["Time Evolution"]["time-dependent"]["mesolve"]["QobjEvo"] =
+ @benchmarkable mesolve($H_td2, $ψ0, $tlist_td, $c_ops, e_ops = $e_ops, progress_bar = Val(false), params = $p)
+
+ SUITE["Time Evolution"]["time-dependent"]["mesolve"]["Liouvillian"] =
+ @benchmarkable mesolve($L_td, $ψ0, $tlist_td, $c_ops, e_ops = $e_ops, progress_bar = Val(false), params = $p)
+
+ ## mcsolve (time-dependent) ##
+
+ SUITE["Time Evolution"]["time-dependent"]["mcsolve"]["Tuple"]["Serial"] = @benchmarkable mcsolve(
+ $H_td,
+ $ψ0,
+ $tlist_td,
+ $c_ops,
+ ntraj = 100,
+ e_ops = $e_ops,
+ progress_bar = Val(false),
+ params = $p,
+ ensemblealg = EnsembleSerial(),
+ )
+
+ SUITE["Time Evolution"]["time-dependent"]["mcsolve"]["Tuple"]["Multithreaded"] = @benchmarkable mcsolve(
+ $H_td,
+ $ψ0,
+ $tlist_td,
+ $c_ops,
+ ntraj = 100,
+ e_ops = $e_ops,
+ progress_bar = Val(false),
+ params = $p,
+ ensemblealg = EnsembleThreads(),
+ )
+
+ SUITE["Time Evolution"]["time-dependent"]["mcsolve"]["QobjEvo"]["Serial"] = @benchmarkable mcsolve(
+ $H_td2,
+ $ψ0,
+ $tlist_td,
+ $c_ops,
+ ntraj = 100,
+ e_ops = $e_ops,
+ progress_bar = Val(false),
+ params = $p,
+ ensemblealg = EnsembleSerial(),
+ )
+
+ SUITE["Time Evolution"]["time-dependent"]["mcsolve"]["QobjEvo"]["Multithreaded"] = @benchmarkable mcsolve(
+ $H_td2,
+ $ψ0,
+ $tlist_td,
+ $c_ops,
+ ntraj = 100,
+ e_ops = $e_ops,
+ progress_bar = Val(false),
+ params = $p,
+ ensemblealg = EnsembleThreads(),
)
return nothing
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 000000000..b3c4f480f
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,5 @@
+build/
+node_modules/
+package-lock.json
+Manifest.toml
+src/resources/changelog.md
\ No newline at end of file
diff --git a/docs/Project.toml b/docs/Project.toml
index 08fe32855..3fc508c90 100644
--- a/docs/Project.toml
+++ b/docs/Project.toml
@@ -1,4 +1,15 @@
[deps]
+BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
+Changelog = "5217a498-cd5d-4ec6-b8c2-9b85a09b6e3e"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
+DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244"
+DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365"
+Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9"
+ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
QuantumToolbox = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab"
+SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1"
+Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
+
+[compat]
+DocumenterVitepress = "0.2"
diff --git a/docs/README.md b/docs/README.md
index e7c090ca1..c8d7ae7e8 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,18 +1,44 @@
-# How to build documentation locally ?
+# How to build documentation and start Vitepress site locally ?
## 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/1/` (which is ignored by git).
+
+## Method 1: Run with `make` command
+Run the following command to instantiate and build the documentation:
+> [!NOTE]
+> You need to install `Node.js` and `npm` first.
+```shell
+make docs
+```
+
+Run the following command to start a local Vitepress site:
+```shell
+make vitepress
+```
+This will start a local Vitepress site of documentation at [http://localhost:5173](http://localhost:5173) in your computer.
+
+## Method 2: Run commands 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:
+> [!NOTE]
+> You need to install `Node.js` and `npm` first.
```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
+
+### Start a local Vitepress site
+Run the following command:
+```shell
+npm --prefix docs run docs:dev
+```
+This will start a local Vitepress site of documentation at [http://localhost:5173](http://localhost:5173) in your computer.
\ No newline at end of file
diff --git a/docs/make.jl b/docs/make.jl
index fccd116db..d9ac8be98 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -3,70 +3,108 @@
using QuantumToolbox
using Documenter
+using DocumenterVitepress
+using DocumenterCitations
+using Changelog
-DocMeta.setdocmeta!(QuantumToolbox, :DocTestSetup, :(using QuantumToolbox); recursive = true)
+# Load of packages required to compile the extension documentation
+using CairoMakie
-const MathEngine = MathJax3(
- Dict(
- :loader => Dict("load" => ["[tex]/physics"]),
- :tex => Dict(
- "inlineMath" => [["\$", "\$"], ["\\(", "\\)"]],
- "tags" => "ams",
- "packages" => ["base", "ams", "autoload", "physics"],
- ),
- )
+doctest_setup = quote
+ using LinearAlgebra
+ using SparseArrays
+ using QuantumToolbox
+end
+DocMeta.setdocmeta!(QuantumToolbox, :DocTestSetup, doctest_setup; recursive = true)
+
+# some options for `makedocs`
+const DRAFT = get(ENV, "DRAFT", false) == "true" # `DRAFT = true` disables cell evaluation
+const DOCTEST = get(ENV, "DOCTEST", true) == true # `DOCTEST = false` skips doc tests
+
+# generate bibliography
+bib = CitationBibliography(
+ joinpath(@__DIR__, "src", "resources", "bibliography.bib"),
+ style=:authoryear,
+)
+
+# generate changelog
+Changelog.generate(
+ Changelog.Documenter(),
+ joinpath(@__DIR__, "..", "CHANGELOG.md"),
+ joinpath(@__DIR__, "src", "resources", "changelog.md");
+ repo = "qutip/QuantumToolbox.jl",
)
const PAGES = [
+ "Home" => "index.md",
"Getting Started" => [
- "Introduction" => "index.md",
- "Key differences from QuTiP" => "qutip_differences.md",
- # "Cite QuantumToolbox.jl" => "cite.md",
+ "Brief Example" => "getting_started/brief_example.md",
+ # "Key differences from QuTiP" => "getting_started/qutip_differences.md",
+ "The Importance of Type-Stability" => "getting_started/type_stability.md",
+ "Example: Create QuantumToolbox.jl Logo" => "getting_started/logo.md",
+ "Cite QuantumToolbox.jl" => "getting_started/cite.md",
],
"Users Guide" => [
"Basic Operations on Quantum Objects" => [
- "users_guide/QuantumObject/QuantumObject.md",
- "users_guide/QuantumObject/QuantumObject_functions.md",
+ "Quantum Objects (Qobj)" => "users_guide/QuantumObject/QuantumObject.md",
+ "Functions operating on Qobj" => "users_guide/QuantumObject/QuantumObject_functions.md",
],
- "The Importance of Type-Stability" => "users_guide/type_stability.md",
"Manipulating States and Operators" => "users_guide/states_and_operators.md",
"Tensor Products and Partial Traces" => "users_guide/tensor.md",
"Time Evolution and Dynamics" => [
"Introduction" => "users_guide/time_evolution/intro.md",
+ "Time Evolution Solutions" => "users_guide/time_evolution/solution.md",
+ "Schrödinger Equation Solver" => "users_guide/time_evolution/sesolve.md",
+ "Lindblad Master Equation Solver" => "users_guide/time_evolution/mesolve.md",
+ "Monte Carlo Solver" => "users_guide/time_evolution/mcsolve.md",
+ "Stochastic Solver" => "users_guide/time_evolution/stochastic.md",
+ "Solving Problems with Time-dependent Hamiltonians" => "users_guide/time_evolution/time_dependent.md",
+ "Bloch-Redfield master equation" => "users_guide/time_evolution/brmesolve.md",
],
- "Solving for Steady-State Solutions" => [],
- "Symmetries" => [],
- "Two-time correlation functions" => [],
+ "Automatic Differentiation" => "users_guide/autodiff.md",
+ "Intensive parallelization on a Cluster" => "users_guide/cluster.md",
+ "Hierarchical Equations of Motion" => "users_guide/HEOM.md",
+ "Solving for Steady-State Solutions" => "users_guide/steadystate.md",
+ "Two-time correlation functions" => "users_guide/two_time_corr_func.md",
+ "Plotting on the Bloch Sphere" => "users_guide/plotting_the_bloch_sphere.md",
+ "QuantumToolbox Settings" => "users_guide/settings.md",
"Extensions" => [
- "users_guide/extensions/cuda.md",
+ "Extension for CUDA.jl" => "users_guide/extensions/cuda.md",
+ "Extension for the Makie.jl ecosystem" => "users_guide/extensions/cairomakie.md",
],
],
- "Tutorials" => [
- "Time Evolution" => [
- "Low Rank Master Equation" => "tutorials/lowrank.md",
- ],
- "Miscellaneous Tutorials" => [
- "tutorials/logo.md",
- ],
+ "Resources" => [
+ "API" => "resources/api.md",
+ "Bibliography" => "resources/bibliography.md",
+ "ChangeLog" => "resources/changelog.md",
+ "Contributing to QuantumToolbox.jl" => "resources/contributing.md",
+ "Acknowledgements" => "resources/acknowledgements.md",
],
- "API" => "api.md",
- # "Change Log" => "changelog.md",
]
makedocs(;
- modules = [QuantumToolbox],
- authors = "Alberto Mercurio, Luca Gravina and Yi-Te Huang",
+ modules = [
+ QuantumToolbox,
+ Base.get_extension(QuantumToolbox, :QuantumToolboxMakieExt),
+ ],
+ authors = "Alberto Mercurio and Yi-Te Huang",
repo = Remotes.GitHub("qutip", "QuantumToolbox.jl"),
sitename = "QuantumToolbox.jl",
pages = PAGES,
- format = Documenter.HTML(;
- prettyurls = get(ENV, "CI", "false") == "true",
- canonical = "https://qutip.github.io/QuantumToolbox.jl",
- edit_link = "main",
- assets = ["assets/favicon.ico"],
- mathengine = MathEngine,
- size_threshold_ignore = ["api.md"],
- )
+ format = DocumenterVitepress.MarkdownVitepress(
+ repo = "github.com/qutip/QuantumToolbox.jl",
+ devbranch = "main",
+ devurl = "dev",
+ ),
+ draft = DRAFT,
+ doctest = DOCTEST,
+ plugins = [bib],
)
-deploydocs(; repo = "github.com/qutip/QuantumToolbox.jl", devbranch = "main")
+DocumenterVitepress.deploydocs(;
+ repo = "github.com/qutip/QuantumToolbox.jl",
+ target = joinpath(@__DIR__, "build"),
+ devbranch = "main",
+ branch = "gh-pages",
+ push_preview = true,
+)
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 000000000..0ed871854
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,15 @@
+{
+ "scripts": {
+ "docs:dev": "vitepress dev build/.documenter",
+ "docs:build": "vitepress build build/.documenter",
+ "docs:preview": "vitepress preview build/.documenter"
+ },
+ "dependencies": {
+ "@nolebase/vitepress-plugin-enhanced-readabilities": "^2.14.0",
+ "markdown-it": "^14.1.0",
+ "markdown-it-footnote": "^4.0.0",
+ "markdown-it-mathjax3": "^4.3.2",
+ "vitepress": "^1.6.3",
+ "vitepress-plugin-tabs": "^0.6.0"
+ }
+}
diff --git a/docs/src/.vitepress/config.mts b/docs/src/.vitepress/config.mts
new file mode 100644
index 000000000..18e32b4b5
--- /dev/null
+++ b/docs/src/.vitepress/config.mts
@@ -0,0 +1,106 @@
+import { defineConfig } from 'vitepress'
+import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
+import mathjax3 from "markdown-it-mathjax3";
+import footnote from "markdown-it-footnote";
+import path from 'path'
+
+function getBaseRepository(base: string): string {
+ if (!base || base === '/') return '/';
+ const parts = base.split('/').filter(Boolean);
+ return parts.length > 0 ? `/${parts[0]}/` : '/';
+}
+
+const baseTemp = {
+ base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs!
+}
+
+const navTemp = {
+ nav: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
+}
+
+const nav = [
+ ...navTemp.nav,
+ { text: 'Tutorials', link: 'https://qutip.org/qutip-julia-tutorials/' },
+ { text: 'Benchmarks', link: 'https://qutip.org/QuantumToolbox.jl/benchmarks/' },
+ {
+ component: 'VersionPicker'
+ }
+]
+
+// https://vitepress.dev/reference/site-config
+export default defineConfig({
+ base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs!
+ title: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
+ description: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
+ lastUpdated: true,
+ cleanUrls: true,
+ outDir: 'REPLACE_ME_DOCUMENTER_VITEPRESS', // This is required for MarkdownVitepress to work correctly...
+ head: [
+ ['link', { rel: 'icon', href: '/QuantumToolbox.jl/favicon.ico' }],
+ ['script', {src: `${getBaseRepository(baseTemp.base)}versions.js`}],
+ // ['script', {src: '/versions.js'], for custom domains, I guess if deploy_url is available.
+ ['script', {src: `${baseTemp.base}siteinfo.js`}]
+ ],
+
+ vite: {
+ define: {
+ __DEPLOY_ABSPATH__: JSON.stringify('REPLACE_ME_DOCUMENTER_VITEPRESS_DEPLOY_ABSPATH'),
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, '../components')
+ }
+ },
+ optimizeDeps: {
+ exclude: [
+ '@nolebase/vitepress-plugin-enhanced-readabilities/client',
+ 'vitepress',
+ '@nolebase/ui',
+ ],
+ },
+ ssr: {
+ noExternal: [
+ // If there are other packages that need to be processed by Vite, you can add them here.
+ '@nolebase/vitepress-plugin-enhanced-readabilities',
+ '@nolebase/ui',
+ ],
+ },
+ },
+ markdown: {
+ math: true,
+
+ // options for @mdit-vue/plugin-toc
+ // https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-toc#options
+ toc: { level: [2, 3, 4] }, // for API page, triggered by: [[toc]]
+
+ config(md) {
+ md.use(tabsMarkdownPlugin),
+ md.use(mathjax3),
+ md.use(footnote)
+ },
+ theme: {
+ light: "github-light",
+ dark: "github-dark"
+ }
+ },
+ themeConfig: {
+ outline: 'deep',
+ logo: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
+ search: {
+ provider: 'local',
+ options: {
+ detailedView: true
+ }
+ },
+ nav,
+ sidebar: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
+ editLink: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
+ socialLinks: [
+ { icon: 'github', link: 'REPLACE_ME_DOCUMENTER_VITEPRESS' }
+ ],
+ footer: {
+ message: 'Made with Documenter.jl, VitePress and DocumenterVitepress.jl
Released under the BSD 3-Clause License. Powered by the Julia Programming Language.
',
+ copyright: `© Copyright ${new Date().getUTCFullYear()} QuTiP.org.`
+ }
+ }
+})
diff --git a/docs/src/.vitepress/theme/style.css b/docs/src/.vitepress/theme/style.css
new file mode 100644
index 000000000..72abe7357
--- /dev/null
+++ b/docs/src/.vitepress/theme/style.css
@@ -0,0 +1,179 @@
+/* Customize default theme styling by overriding CSS variables:
+https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
+/* Example */
+/* https://github.com/vuejs/vitepress/blob/main/template/.vitepress/theme/style.css */
+
+.VPHero .clip {
+ white-space: pre;
+ max-width: 600px;
+}
+
+/* Fonts */
+@font-face {
+ font-family: JuliaMono-Regular;
+ src: url("https://cdn.jsdelivr.net/gh/cormullion/juliamono/webfonts/JuliaMono-Regular.woff2");
+}
+
+:root {
+/* Typography */
+--vp-font-family-base: "Barlow", "Inter var experimental", "Inter var",
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
+ Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+
+/* Code Snippet font */
+--vp-font-family-mono: JuliaMono-Regular, monospace;
+}
+
+/* Disable contextual alternates (kind of like ligatures but different) in monospace,
+ which turns `/>` to an up arrow and `|>` (the Julia pipe symbol) to an up arrow as well. */
+.mono-no-substitutions {
+font-family: "JuliaMono-Regular", monospace;
+font-feature-settings: "calt" off;
+}
+
+.mono-no-substitutions-alt {
+font-family: "JuliaMono-Regular", monospace;
+font-variant-ligatures: none;
+}
+
+pre, code {
+font-family: "JuliaMono-Regular", monospace;
+font-feature-settings: "calt" off;
+}
+
+/* Colors */
+:root {
+ --julia-blue: #4063D8;
+ --julia-purple: #9558B2;
+ --julia-red: #CB3C33;
+ --julia-green: #389826;
+
+ --vp-c-brand: #0087d7;
+ --vp-c-brand-1: #0890df;
+ --vp-c-brand-2: #0599ef;
+ --vp-c-brand-3: #0c9ff4;
+ --vp-c-brand-light: #0087d7;
+ --vp-c-brand-dark: #5fd7ff;
+ --vp-c-brand-dimm: #212425;
+
+ /* Greens */
+ --vp-dark-green: #155f3e; /* Main accent green */
+ --vp-dark-green-dark: #2b855c;
+ --vp-dark-green-light: #42d392;
+ --vp-dark-green-lighter: #35eb9a;
+ /* Complementary Colors */
+ --vp-dark-gray: #1e1e1e;
+ --vp-dark-gray-soft: #2a2a2a;
+ --vp-dark-gray-mute: #242424;
+ --vp-light-gray: #d1d5db;
+ --vp-tip-bg: rgb(254, 254, 254);
+
+ /* Text Colors */
+ --vp-dark-text: #e5e5e5; /* Primary text color */
+ --vp-dark-subtext: #c1c1c1; /* Subtle text */
+ --vp-source-text: #e5e5e5;
+ /* custom tip */
+ --vp-custom-block-tip-border: var(--vp-c-brand-light);
+ --vp-custom-block-tip-bg: var(--vp-tip-bg);
+}
+
+ /* Component: Button */
+:root {
+ --vp-button-brand-border: var(--vp-light-gray);
+ --vp-button-brand-bg: var(--vp-c-brand-light);
+ --vp-button-brand-hover-border: var(--vp-c-bg-alt);
+ --vp-button-brand-hover-bg: var(--julia-blue);
+}
+
+/* Component: Home */
+:root {
+ --vp-home-hero-name-color: transparent;
+ --vp-home-hero-name-background: -webkit-linear-gradient(
+ 120deg,
+ #9558B2 30%,
+ #CB3C33
+ );
+
+ --vp-home-hero-image-background-image: none; /* remove the blur background */
+ /* (default setting)
+ --vp-home-hero-image-background-image: linear-gradient(
+ -145deg,
+ #9558b282 30%,
+ #3798269a 30%,
+ #cb3d33e3
+ );
+ */
+ --vp-home-hero-image-filter: blur(40px);
+}
+
+/* Hero Section */
+:root.dark {
+ --vp-home-hero-name-color: transparent;
+ --vp-home-hero-name-background: -webkit-linear-gradient(
+ 120deg,
+ #9558B2 30%,
+ #CB3C33
+ );
+ --vp-home-hero-image-background-image: none; /* remove the blur background */
+ /* (default setting)
+ --vp-home-hero-image-background-image: linear-gradient(
+ -45deg,
+ var(--vp-dark-green) 30%,
+ var(--vp-dark-green-light),
+ var(--vp-dark-gray) 30%
+ );
+ */
+ --vp-home-hero-image-filter: blur(56px);
+}
+
+:root.dark {
+ /* custom tip */
+ --vp-custom-block-tip-border: var(--vp-dark-green-dark);
+ --vp-custom-block-tip-text: var(--vp-dark-subtext);
+ --vp-custom-block-tip-bg: var(--vp-dark-gray-mute);
+}
+
+/**
+ * Colors links
+ * -------------------------------------------------------------------------- */
+
+.dark {
+ --vp-c-brand: var(--vp-dark-green-light);
+ --vp-button-brand-border: var(--vp-dark-green-lighter);
+ --vp-button-brand-bg: var(--vp-dark-green);
+ --vp-c-brand-1: var(--vp-dark-green-light);
+ --vp-c-brand-2: var(--vp-dark-green-lighter);
+ --vp-c-brand-3: var(--vp-dark-green);
+}
+
+@media (min-width: 640px) {
+ :root {
+ --vp-home-hero-image-filter: blur(56px);
+ }
+}
+
+@media (min-width: 960px) {
+ :root {
+ --vp-home-hero-image-filter: blur(72px);
+ }
+}
+/* Component: MathJax */
+
+mjx-container > svg {
+ display: block;
+ margin: auto;
+}
+
+mjx-container {
+ padding: 0.5rem 0;
+}
+
+mjx-container {
+ display: inline;
+ margin: auto 2px -2px;
+}
+
+mjx-container > svg {
+ margin: auto;
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png
old mode 100755
new mode 100644
diff --git a/docs/src/getting_started/brief_example.md b/docs/src/getting_started/brief_example.md
new file mode 100644
index 000000000..b2a280753
--- /dev/null
+++ b/docs/src/getting_started/brief_example.md
@@ -0,0 +1,86 @@
+```@meta
+CurrentModule = QuantumToolbox
+```
+
+# Brief Example
+
+We now provide a brief example to demonstrate the similarity between [`QuantumToolbox.jl`](https://github.com/qutip/QuantumToolbox.jl) and [`QuTiP`](https://github.com/qutip/qutip).
+
+## CPU Computation
+
+Let's consider a quantum harmonic oscillator with a Hamiltonian given by:
+
+```math
+\hat{H} = \omega \hat{a}^\dagger \hat{a}
+```
+
+where ``\hat{a}`` and ``\hat{a}^\dagger`` are the annihilation and creation operators, respectively. We can define the Hamiltonian as follows:
+
+```julia
+using QuantumToolbox
+
+N = 20 # cutoff of the Hilbert space dimension
+ω = 1.0 # frequency of the harmonic oscillator
+
+a = destroy(N) # annihilation operator
+
+H = ω * a' * a
+```
+
+We now introduce some losses in a thermal environment, described by the Lindblad master equation:
+
+```math
+\frac{d \hat{\rho}}{dt} = -i [\hat{H}, \hat{\rho}] + \gamma \mathcal{D}[\hat{a}] \hat{\rho}
+```
+
+where ``\hat{\rho}`` is the density matrix, ``\gamma`` is the damping rate, and ``\mathcal{D}[\hat{a}]`` is the Lindblad dissipator, defined as:
+
+```math
+\mathcal{D}[\hat{a}]\hat{\rho} = \hat{a}\hat{\rho}\hat{a}^\dagger - \frac{1}{2}\hat{a}^\dagger\hat{a}\hat{\rho} - \frac{1}{2}\hat{\rho}\hat{a}^\dagger\hat{a}
+```
+
+!!! note "Lindblad master equation"
+ See [here](@ref doc-TE:Lindblad-Master-Equation-Solver) for more details about Lindblad master equation.
+
+We now compute the time evolution of the system using the [`mesolve`](@ref) function, starting from the initial state ``\ket{\psi (0)} = \ket{3}``:
+
+```julia
+γ = 0.1 # damping rate
+
+ψ0 = fock(N, 3) # initial state
+
+tlist = range(0, 10, 100) # time list
+
+c_ops = [sqrt(γ) * a]
+e_ops = [a' * a]
+
+sol = mesolve(H, ψ0, tlist, c_ops, e_ops = e_ops)
+```
+
+We can extract the expectation value of the number operator ``\hat{a}^\dagger \hat{a}`` with the command `sol.expect`, and the states with the command `sol.states`.
+
+## GPU Computation
+
+!!! note "Extension for CUDA.jl"
+ `QuantumToolbox.jl` provides an extension to support GPU computation. To trigger the extension, you need to install and import [`CUDA.jl`](https://github.com/JuliaGPU/CUDA.jl) together with `QuantumToolbox.jl`. See [here](@ref doc:CUDA) for more details.
+
+```julia
+using QuantumToolbox
+using CUDA
+CUDA.allowscalar(false) # Avoid unexpected scalar indexing
+```
+
+We can easily pass the computation to the GPU, by simply passing all the [`QuantumObject`](@ref)s to the GPU:
+
+```julia
+a_gpu = cu(destroy(N)) # The only difference in the code is the cu() function
+
+H_gpu = ω * a_gpu' * a_gpu
+
+ψ0_gpu = cu(fock(N, 3))
+
+c_ops = [sqrt(γ) * a_gpu]
+e_ops = [a_gpu' * a_gpu]
+
+sol = mesolve(H_gpu, ψ0_gpu, tlist, c_ops, e_ops = e_ops)
+```
\ No newline at end of file
diff --git a/docs/src/getting_started/cite.md b/docs/src/getting_started/cite.md
new file mode 100644
index 000000000..7212e5727
--- /dev/null
+++ b/docs/src/getting_started/cite.md
@@ -0,0 +1,19 @@
+# [Cite QuantumToolbox.jl](@id doc:Cite)
+
+If you like `QuantumToolbox.jl`, we would appreciate it if you could cite our publication [ [Quantum 9, 1866 (2025)](https://doi.org/10.22331/q-2025-09-29-1866) ] using the following bibtex entry:
+
+```bib
+@article{QuantumToolbox.jl2025,
+ title = {Quantum{T}oolbox.jl: {A}n efficient {J}ulia framework for simulating open quantum systems},
+ author = {Mercurio, Alberto and Huang, Yi-Te and Cai, Li-Xun and Chen, Yueh-Nan and Savona, Vincenzo and Nori, Franco},
+ journal = {{Quantum}},
+ issn = {2521-327X},
+ publisher = {{Verein zur F{\"{o}}rderung des Open Access Publizierens in den Quantenwissenschaften}},
+ volume = {9},
+ pages = {1866},
+ month = sep,
+ year = {2025},
+ doi = {10.22331/q-2025-09-29-1866},
+ url = {https://doi.org/10.22331/q-2025-09-29-1866}
+}
+```
diff --git a/docs/src/tutorials/logo.md b/docs/src/getting_started/logo.md
similarity index 63%
rename from docs/src/tutorials/logo.md
rename to docs/src/getting_started/logo.md
index ac39523e7..ae3093bff 100644
--- a/docs/src/tutorials/logo.md
+++ b/docs/src/getting_started/logo.md
@@ -1,8 +1,8 @@
-# [Create QuantumToolbox.jl logo](@id doc-tutor:Create-QuantumToolbox.jl-logo)
+# [Example: Create QuantumToolbox.jl logo](@id doc:Create-QuantumToolbox.jl-logo)
## Introduction
-In this tutorial, we will demonstrate how to create the logo for the **QuantumToolbox.jl** package. The logo represents the Wigner function of the triangular cat state, which is a linear superposition of three coherent states. The resulting Wigner function has a triangular shape that resembles the Julia logo. We will also define a custom colormap that varies based on the value of the Wigner function and the spatial coordinates, such that the three blobs corresponding to the coherent states have different colors (matching the colors of the Julia logo).
+In this example, we will demonstrate how to create the logo for the **QuantumToolbox.jl** package. The logo represents the Wigner function of the triangular cat state, which is a linear superposition of three coherent states. The resulting Wigner function has a triangular shape that resembles the Julia logo. We will also define a custom colormap that varies based on the value of the Wigner function and the spatial coordinates, such that the three blobs corresponding to the coherent states have different colors (matching the colors of the Julia logo).
### Triangular Cat State
@@ -14,23 +14,23 @@ A cat state, often referred to as a Schrödinger cat state, is a quantum state t
where ``| \alpha \rangle`` is a coherent state with amplitude ``\alpha``.
-The triangular cat state is a generalization of the standard cat state. It is a superposition of three coherent states with phases ``\theta_0, \theta_1, \theta_2`` separated by ``120^\circ``(or ``2\pi/3``radians):
+The triangular cat state is a generalization of the standard cat state. It is a superposition of three coherent states with phases ``\theta_0, \theta_1, \theta_2`` separated by ``120^\circ`` (or ``2\pi/3``radians):
```math
| \psi_{\text{tri-cat}} \rangle = \frac{1}{\sqrt{3}} \left( | \alpha_0 \rangle + | \alpha_1 \rangle + | \alpha_2 \rangle \right)
```
-where ``\alpha_j = \rho e^{i\theta_j}``with ``\theta_j = \frac{\pi}{2} + \frac{2\pi j}{3}``and ``j = 0, 1, 2``.
+where ``\alpha_j = \rho e^{i\theta_j}`` with ``\theta_j = \frac{\pi}{2} + \frac{2\pi j}{3}`` and ``j = 0, 1, 2``.
### Wigner Function
-The Wigner function ``W(x, p)``is a quasi-probability distribution used in quantum mechanics to represent quantum states in phase space. It is defined as:
+The Wigner function ``W(x, p)`` is a quasi-probability distribution used in quantum mechanics to represent quantum states in phase space. It is defined as:
```math
W(x, p) = \frac{1}{\pi \hbar} \int_{-\infty}^{\infty} \psi^*(x + y) \psi(x - y) e^{2ipy / \hbar} \, dy
```
-where ``\psi(x)``is the wave function of the quantum state, ``x``is the position, ``p``is the momentum, and ``\hbar``is the reduced Planck constant. Unlike classical probability distributions, the Wigner function can take negative values, which indicates non-classical behavior.
+where ``\psi(x)`` is the wave function of the quantum state, ``x`` is the position, ``p`` is the momentum, and ``\hbar`` is the reduced Planck constant. Unlike classical probability distributions, the Wigner function can take negative values, which indicates non-classical behavior.
## Generating the Logo
@@ -67,25 +67,16 @@ Next, we construct the triangular cat state as a normalized superposition of thr
normalize!(ψ)
```
-### Defining the Grid and calculating the Wigner function
+### Defining the Grid and plotting the Wigner function
-We define the grid for the Wigner function and calculate it using the [`wigner`](@ref) function. We shift the grid in the imaginary direction to ensure that the Wigner function is centered around the origin of the figure. The [`wigner`](@ref) function also supports the `g` scaling factor, which we put here equal to ``2``.
+We define the grid for the Wigner function and plot it using the [`plot_wigner`](@ref) function. This, internally calls the [`wigner`](@ref) function for the computation. We shift the grid in the imaginary direction to ensure that the Wigner function is centered around the origin of the figure. The [`wigner`](@ref) function also supports the `g` scaling factor, which we put here equal to ``2``.
```@example logo
xvec = range(-ρ, ρ, 500) .* 1.5
yvec = xvec .+ (abs(imag(α1)) - abs(imag(α2))) / 2
-wig = wigner(ψ, xvec, yvec, g = 2)
-```
-
-### Plotting the Wigner function
-
-Finally, we plot the Wigner function using the `heatmap` function from the `CairoMakie` package.
-
-```@example logo
-fig = Figure(size = (500, 500), figure_padding = 0)
-ax = Axis(fig[1, 1])
-heatmap!(ax, xvec, yvec, wig', colormap = :RdBu, interpolate = true, rasterize = 1)
+fig = Figure(size = (250, 250), figure_padding = 0)
+fig, ax, hm = plot_wigner(ψ, xvec = xvec, yvec = yvec, g = 2, library = Val(:Makie), location = fig[1,1])
hidespines!(ax)
hidexdecorations!(ax)
hideydecorations!(ax)
@@ -100,7 +91,7 @@ The figure obtained above coulb be already a potential logo for the package. How
\frac{d \hat{\rho}}{dt} = -i [\hat{H}, \hat{\rho}] + \gamma \left( 2 \hat{a} \hat{\rho} \hat{a}^\dagger - \hat{a}^\dagger \hat{a} \hat{\rho} - \hat{\rho} \hat{a}^\dagger \hat{a} \right)
```
-where ``\hat{\rho}`` is the density matrix, ``\hat{H} = \omega \hat{a}^\dagger \hat{a}``is the Hamiltonian of the harmonic oscillator (``\hbar = 1``), ``\hat{a}``and ``\hat{a}^\dagger``are the annihilation and creation operators, and ``\gamma``is the damping rate. Thus, we initialize the system in the triangular cat state and evolve it under the Lindblad master equation, using the [`mesolve`](@ref) function.
+where ``\hat{\rho}`` is the density matrix, ``\hat{H} = \omega \hat{a}^\dagger \hat{a}`` is the Hamiltonian of the harmonic oscillator (``\hbar = 1``), ``\hat{a}`` and ``\hat{a}^\dagger`` are the annihilation and creation operators, and ``\gamma`` is the damping rate. Thus, we initialize the system in the triangular cat state and evolve it under the [Lindblad master equation](@ref doc-TE:Lindblad-Master-Equation-Solver), using the [`mesolve`](@ref) function.
```@example logo
γ = 0.012
@@ -118,12 +109,8 @@ nothing # hide
And the Wigner function becomes more uniform:
```@example logo
-wig = wigner(sol.states[end], xvec, yvec, g = 2)
-
-fig = Figure(size = (500, 500), figure_padding = 0)
-ax = Axis(fig[1, 1])
-
-img_wig = heatmap!(ax, xvec, yvec, wig', colormap = :RdBu, interpolate = true, rasterize = 1)
+fig = Figure(size = (250, 250), figure_padding = 0)
+fig, ax, hm = plot_wigner(sol.states[end], xvec = xvec, yvec = yvec, g = 2, library = Val(:Makie), location = fig[1,1])
hidespines!(ax)
hidexdecorations!(ax)
hideydecorations!(ax)
@@ -135,7 +122,7 @@ At this stage, we have finished to use the `QuantumToolbox` package. From now on
### Custom Colormap
-We define a custom colormap that changes depending on the Wigner function and spatial coordinates. Indeed, we want the three different colormaps, in the regions corresponding to the three coherent states, to match the colors of the Julia logo. We also want the colormap change to be smooth, so we use a Gaussian function to blend the colors. We introduce also a Wigner function dependent transparency to make the logo more appealing.
+We define a custom colormap that changes depending on the Wigner function and spatial coordinates. Indeed, we want the three different colormaps, in the regions corresponding to the three coherent states, to match the colors of the Julia logo. We also want the colormap change to be smooth, so we use a Gaussian function to blend the colors. We introduce also a Wigner function dependent transparency to make the logo more appealing. In order to do so, we are going to need the value of the wigner function at each point of the grid, rather than its plot. We will thus call the [`wigner`](@ref) function directly.
```@example logo
function set_color_julia(x, y, wig::T, α1, α2, α3, cmap1, cmap2, cmap3, δ) where {T}
@@ -156,6 +143,7 @@ function set_color_julia(x, y, wig::T, α1, α2, α3, cmap1, cmap2, cmap3, δ) w
return RGBAf(c_tot.r, c_tot.g, c_tot.b, alpha)
end
+wig = wigner(sol.states[end], xvec, yvec, g = 2)
X, Y = meshgrid(xvec, yvec)
δ = 1.25 # Smoothing parameter for the Gaussian functions
```
@@ -178,7 +166,7 @@ cmap3 = cgrad(vcat(fill(julia_blue, n_repeats), fill(julia_purple, n_repeats)))
### Normalizing the Wigner function and applying the custom colormap
-The colormaps require the input to be in the range ``[0, 1]``. We normalize the Wigner function such that the maximum value is ``1``and the zeros are set to ``0.5``.
+The colormaps require the input to be in the range ``[0, 1]``. We normalize the Wigner function such that the maximum value is ``1`` and the zeros are set to ``0.5``.
```@example logo
vmax = maximum(wig)
@@ -197,7 +185,7 @@ img = set_color_julia.(X, Y, wig_normalized, α1, α2, α3, Ref(cmap1), Ref(cmap
Finally, we plot the Wigner function with the custom colormap.
```@example logo
-fig = Figure(size = (500, 500), figure_padding = 0, backgroundcolor = :transparent)
+fig = Figure(size = (250, 250), figure_padding = 0, backgroundcolor = :transparent)
ax = Axis(fig[1, 1], backgroundcolor = :transparent)
image!(ax, img', rasterize = 1)
hidespines!(ax)
@@ -208,4 +196,4 @@ fig
## Conclusion
-This tutorial demonstrates how to generate the [QuantumToolbox.jl](https://github.com/qutip/QuantumToolbox.jl) logo using the package itself and [Makie.jl](https://github.com/MakieOrg/Makie.jl) for visualization. The logo is a visualization of the Wigner function of a triangular cat state, with a custom colormap that highlights the different coherent states with colors matching the Julia logo.
+This example demonstrates how to generate the [QuantumToolbox.jl](https://github.com/qutip/QuantumToolbox.jl) logo using the package itself and [Makie.jl](https://github.com/MakieOrg/Makie.jl) for visualization. The logo is a visualization of the Wigner function of a triangular cat state, with a custom colormap that highlights the different coherent states with colors matching the Julia logo.
diff --git a/docs/src/qutip_differences.md b/docs/src/getting_started/qutip_differences.md
similarity index 100%
rename from docs/src/qutip_differences.md
rename to docs/src/getting_started/qutip_differences.md
diff --git a/docs/src/getting_started/type_stability.md b/docs/src/getting_started/type_stability.md
new file mode 100644
index 000000000..bd22d3481
--- /dev/null
+++ b/docs/src/getting_started/type_stability.md
@@ -0,0 +1,288 @@
+# [The Importance of Type-Stability](@id doc:Type-Stability)
+
+You are here because you have probably heard about the excellent performance of Julia compared to other common programming languages like Python. One of the reasons is the Just-In-Time (JIT) compiler of Julia, which is able to generate highly optimized machine code. However, the JIT compiler can only do its job if the code type can be inferred. You can also read the [Performance Tips](https://docs.julialang.org/en/v1/manual/performance-tips/) section in Julia's documentation for more details. Here, we try to explain it briefly, with a focus on the `QuantumToolbox.jl` package.
+
+!!! note
+ This page is not a tutorial on `QuantumToolbox.jl`, but rather a general guide to writing Julia code for simulating quantum systems efficiently. If you don't care about the performance of your code, you can skip this page.
+
+## Basics of type stability
+
+Let's have a look at the following example:
+
+```@setup type-stability
+using InteractiveUtils
+using QuantumToolbox
+```
+
+```@example type-stability
+function foo(x)
+ if x > 0
+ return 1
+ else
+ return -1.0
+ end
+end
+nothing # hide
+```
+
+The function `foo` apparently seems to be innocent. It takes an argument `x` and returns either `1` or `-1.0` depending on the sign of `x`. However, the return type of `foo` is not clear. If `x` is positive, the return type is `Int`, otherwise it is `Float64`. This is a problem for the JIT compiler, because it has to determine the return type of `foo` at runtime. This is called type instability (even though it is a weak form) and may lead to a significant performance penalty. To avoid this, always aim for type-stable code. This means that the return type of a function should be clear from the types of its arguments. We can check the inferred return type of `foo` using the `@code_warntype` macro:
+
+```@example type-stability
+@code_warntype foo(1)
+```
+
+The key point is to ensure the return type of a function is clear from the types of its arguments. There are several ways to achieve this, and the best approach depends on the specific problem. For example, one can use the same return type:
+
+```@example type-stability
+function foo(x)
+ if x > 0
+ return 1.0
+ else
+ return -1.0
+ end
+end
+nothing # hide
+```
+
+Or you can ensure the return type matches the type of the argument:
+
+```@example type-stability
+function foo(x::T) where T
+ if x > 0
+ return T(1)
+ else
+ return -T(1)
+ end
+end
+nothing # hide
+```
+
+The latter example is very important because it takes advantage of Julia's multiple dispatch, which is one of the most powerful features of the language. Depending on the type `T` of the argument `x`, the Julia compiler generates a specialized version of `foo` that is optimized for this type. If the input type is an `Int64`, the return type is `Int64`, if `x` is a `Float64`, the return type is `Float64`, and so on.
+
+```@example type-stability
+@show foo(1)
+@show foo(-4.4)
+@show foo(1//2)
+nothing # hide
+```
+
+!!! note
+ If you didn't know how to make this function type-stable, it is probably a good idea to read the official Julia documentation, and in particular its [Performance Tips](https://docs.julialang.org/en/v1/manual/performance-tips/) section.
+
+## Global variables
+
+Another source of type instability is the use of global variables. In general, it is a good idea to declare global variables as `const` to ensure their type is fixed for the entire program. For example, consider the following function that internally takes a global variable `y`:
+
+```@example type-stability
+y = 2.4
+
+function bar(x)
+ res = zero(x) # this returns the zero of the same type of x
+ for i in 1:1000
+ res += y * x
+ end
+ return res
+end
+nothing # hide
+```
+
+The Julia compiler cannot infer the type of `res` because it depends on the type of `y`, which is a global variable that can change at any time of the program. We can check it using the `@code_warntype` macro:
+
+```@example type-stability
+@code_warntype bar(3.2)
+```
+
+While in the last example of the `foo` function we got a weak form of type instability, returning a `Union{Int, Float64}`, in this case the return type of `bar` is `Any`, meaning that the compiler doesn't know anything about the return type. Thus, this function has nothing different from a dynamically typed language like Python. We can benchmark the performance of `bar` using the [BenchmarkTools.jl](https://github.com/JuliaCI/BenchmarkTools.jl) package:
+
+```@example type-stability
+using BenchmarkTools
+
+@benchmark bar(3.2)
+```
+
+Here we see a lot of memory allocations and low performances in general. To fix this, we can declare a `const` (constant) variable instead:
+
+```@example type-stability
+const z = 2.4
+
+function bar(x)
+ res = zero(x) # this returns the zero of the same type of x
+ for i in 1:1000
+ res += z * x
+ end
+ return res
+end
+
+@benchmark bar(3.2)
+```
+
+And we can see that the performance has improved significantly. Hence, we highly recommend using global variables as `const`, but only when truly necessary. This choice is problem-dependent, but in the case of `QuantumToolbox.jl`, this can be applied for example in the case of defining the Hilbert space dimensions, static parameters, or the system operators.
+
+Although it is always a good practice to avoid such kind of type instabilities, in the actual implementation of `QuantumToolbox.jl` (where we mainly deal with linear algebra operations), the compiler may perform only a few runtime dispatches, and the performance penalty may be negligible compared to the heavy linear algebra operations.
+
+## Vectors vs Tuples vs StaticArrays
+
+Julia has many ways to represent arrays or lists of general objects. The most common are `Vector`s and `Tuple`s. The former is a dynamic array that can change its size at runtime, while the latter is a fixed-size array that is immutable, and where the type of each element is already known at compile time. For example:
+
+```@example type-stability
+v1 = [1, 2, 3] # Vector of Int64
+v2 = [1.0 + 2.0im, 3.0 + 4.0im] # Vector of ComplexF64
+v3 = [1, "ciao", 3.0] # Vector of Any
+
+t1 = (1, 2, 3) # Tuple of {Int64, Int64, Int64}
+t2 = (1.0 + 2.0im, 3.0 + 4.0im) # Tuple of {ComplexF64, ComplexF64}
+t3 = (1, "ciao", 3.0) # Tuple of {Int64, String, Float64}
+
+@show typeof(v1)
+@show typeof(v2)
+@show typeof(v3)
+@show typeof(t1)
+@show typeof(t2)
+@show typeof(t3)
+nothing # hide
+```
+
+Thus, we highly recommend using `Vector` only when we are sure that it contains elements of the same type, and only when we don't need to know its size at compile time. On the other hand, `Tuple`s are less flexible but more efficient in terms of performance. A third option is to use the `SVector` type from the [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) package. This is similar to `Vector`, where the elements should have the same type, but it is fixed-size and immutable. One may ask when it is necessary to know the array size at compile time. A practical example is the case of [`ptrace`](@ref), where it internally reshapes the quantum state into a tensor whose dimensions depend on the number of subsystems. We will see this in more detail in the next section.
+
+## The `QuantumObject` internal structure
+
+Before making a practical example, let's see the internal structure of the [`QuantumObject`](@ref) type. As an example, we consider the case of three qubits, and we study the internal structure of the ``\hat{\sigma}_x^{(2)}`` operator:
+
+```@example type-stability
+σx_2 = tensor(qeye(2), sigmax(), qeye(2))
+```
+
+and its type is
+
+```@example type-stability
+obj_type = typeof(σx_2)
+```
+
+This is exactly what the Julia compiler sees: it is a [`QuantumObject`](@ref), composed by a field of type `SparseMatrixCSC{ComplexF64, Int64}` (i.e., the 8x8 matrix containing the Pauli matrix, tensored with the identity matrices of the other two qubits). Then, we can also see that it is a [`Operator`](@ref), with `3` subsystems in total. Hence, just looking at the type of the object, the compiler has all the information it needs to generate a specialized version of the functions.
+
+Let's see more in the details all the internal fields of the [`QuantumObject`](@ref) type:
+
+```@example type-stability
+fieldnames(obj_type)
+```
+
+```@example type-stability
+σx_2.data
+```
+
+```@example type-stability
+σx_2.type
+```
+
+
+```@example type-stability
+σx_2.dims
+```
+
+The `dims` field contains the dimensions of the subsystems (in this case, three subsystems with dimension `2` each). We can see that the type of `dims` is `SVector` instead of `Vector`. As we mentioned before, this is very useful in functions like [`ptrace`](@ref). Let's do a simple example of reshaping an operator internally generated from some `dims` input:
+
+```@example type-stability
+function reshape_operator_data(dims)
+ op = Qobj(randn(prod(dims), prod(dims)), type=Operator(), dims=dims)
+ op_dims = op.dims
+ op_data = op.data
+ return reshape(op_data, vcat(op_dims, op_dims)...)
+end
+
+typeof(reshape_operator_data([2, 2, 2]))
+```
+
+Which returns a tensor of size `2x2x2x2x2x2`. Let's check the `@code_warntype`:
+
+```@example type-stability
+@code_warntype reshape_operator_data([2, 2, 2])
+```
+
+We got a `Any` type, because the compiler doesn't know the size of the `dims` vector. We can fix this by using a `Tuple` (or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl)):
+
+```@example type-stability
+typeof(reshape_operator_data((2, 2, 2)))
+```
+
+```@example type-stability
+@code_warntype reshape_operator_data((2, 2, 2))
+```
+
+Finally, let's look at the benchmarks
+
+```@example type-stability
+@benchmark reshape_operator_data($[2, 2, 2])
+```
+
+```@example type-stability
+@benchmark reshape_operator_data($((2, 2, 2)))
+```
+
+Which is an innocuous but huge difference in terms of performance. Hence, we highly recommend using `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) when defining the dimensions of a user-defined [`QuantumObject`](@ref).
+
+## The use of `Val` in some `QuantumToolbox.jl` functions
+
+In some functions of `QuantumToolbox.jl`, you may find the use of the [`Val`](https://docs.julialang.org/en/v1/base/base/#Base.Val) type in the arguments. This is a trick to pass a value at compile time, and it is very useful to avoid type instabilities. Let's make a very simple example, where we want to create a Fock state ``|j\rangle`` of a given dimension `N`, and we give the possibility to create it as a sparse or dense vector. At first, we can write the function without using `Val`:
+
+```@example type-stability
+using SparseArrays
+
+function my_fock(N::Int, j::Int = 0; sparse::Bool = false)
+ if sparse
+ array = sparsevec([j + 1], [1.0 + 0im], N)
+ else
+ array = zeros(ComplexF64, N)
+ array[j+1] = 1
+ end
+ return QuantumObject(array; type = Ket())
+end
+@show my_fock(2, 1)
+@show my_fock(2, 1; sparse = true)
+nothing # hide
+```
+
+But it is immediately clear that the return type of this function is not clear, because it depends on the value of the `sparse` argument. We can check it using the `@code_warntype` macro:
+
+```@example type-stability
+@code_warntype my_fock(2, 1)
+```
+
+```@example type-stability
+@code_warntype my_fock(2, 1; sparse = true)
+```
+
+We can fix this by using the `Val` type, where we enable the multiple dispatch of the function:
+
+```@example type-stability
+getVal(::Val{N}) where N = N
+function my_fock_good(N::Int, j::Int = 0; sparse::Val = Val(false))
+ if getVal(sparse)
+ array = zeros(ComplexF64, N)
+ array[j+1] = 1
+ else
+ array = sparsevec([j + 1], [1.0 + 0im], N)
+ end
+ return QuantumObject(array; type = Ket())
+end
+@show my_fock_good(2, 1)
+@show my_fock_good(2, 1; sparse = Val(true))
+nothing # hide
+```
+
+And now the return type of the function is clear:
+
+```@example type-stability
+@code_warntype my_fock_good(2, 1)
+```
+
+```@example type-stability
+@code_warntype my_fock_good(2, 1; sparse = Val(true))
+```
+
+This is exactly how the current [`fock`](@ref) function is implemented in `QuantumToolbox.jl`. There are many other functions that support this feature, and we highly recommend using it when necessary.
+
+## Conclusions
+
+In this page, we have seen the importance of type stability in Julia, and how to write efficient code in the context of `QuantumToolbox.jl`. We have seen that the internal structure of the [`QuantumObject`](@ref) type is already optimized for the compiler, and we have seen some practical examples of how to write efficient code. We have seen that the use of `Vector` should be avoided when the elements don't have the same type, and that the use of `Tuple` or `SVector` is highly recommended when the size of the array is known at compile time. Finally, we have seen the use of `Val` to pass values at compile time, to avoid type instabilities in some functions.
+```
+
diff --git a/docs/src/index.md b/docs/src/index.md
index 149df2998..a8f3baaa8 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -1,24 +1,65 @@
-```@meta
-CurrentModule = QuantumToolbox
+```@raw html
+---
+# https://vitepress.dev/reference/default-theme-home-page
+layout: home
+
+hero:
+ name: "QuantumToolbox.jl"
+ tagline: A pure Julia framework designed for high-performance quantum physics simulations
+ image:
+ src: /logo.png
+ alt: QuantumToolbox
+ actions:
+ - theme: brand
+ text: Getting Started
+ link: /getting_started/brief_example
+ - theme: alt
+ text: Users Guide
+ link: /users_guide/QuantumObject/QuantumObject
+ - theme: alt
+ text: Tutorials
+ link: https://qutip.org/qutip-julia-tutorials/
+ - theme: alt
+ text: API
+ link: /resources/api
+ - theme: alt
+ text: Cite us
+ link: /getting_started/cite
+ - theme: alt
+ text: View on Github
+ link: https://github.com/qutip/QuantumToolbox.jl
+ - theme: alt
+ text: Visit QuTiP.org
+ link: https://qutip.org/
+
+
+features:
+ - icon:
+ title: Dynamical Evolution
+ details: Advanced solvers for time evolution of quantum systems, thanks to the powerful DifferentialEquations.jl package.
+ link: /users_guide/time_evolution/intro
+ - icon:
+ title: GPU Computing
+ details: Leverage GPU resources for high-performance computing. Simulate quantum dynamics directly on the GPU with the same syntax as the CPU case.
+ link: /users_guide/extensions/cuda
+ - icon:
+ title: Distributed Computing
+ details: Distribute the computation over multiple nodes (e.g., a cluster). Simulate hundreds of quantum trajectories in parallel on a cluster, with, again, the same syntax as the simple case.
+ link: /users_guide/cluster
+ - icon:
+ title: Differentiable Programming
+ details: Enable gradient-based optimization for quantum algorithms. Compute gradients of quantum dynamics with respect to their parameters using automatic differentiation.
+ link: /users_guide/autodiff
+---
```
-# QuantumToolbox.jl Documentation
+# [Introduction](@id doc:Introduction)
-[QuantumToolbox.jl](https://github.com/qutip/QuantumToolbox.jl) is a cutting-edge Julia package designed for quantum physics simulations, closely emulating the popular Python [QuTiP](https://github.com/qutip/qutip) package. It uniquely combines the simplicity and power of Julia with advanced features like GPU acceleration and distributed computing, making simulation of quantum systems more accessible and efficient.
+[`QuantumToolbox.jl`](https://github.com/qutip/QuantumToolbox.jl) is a cutting-edge [`Julia`](https://julialang.org/) package designed for quantum physics simulations, closely emulating the popular [`Python QuTiP`](https://github.com/qutip/qutip) package. It uniquely combines the simplicity and power of Julia with advanced features like GPU acceleration and distributed computing, making simulation of quantum systems more accessible and efficient. Taking advantage of the [`Julia`](https://julialang.org/) language features (like multiple dispatch and metaprogramming), [`QuantumToolbox.jl`](https://github.com/qutip/QuantumToolbox.jl) is designed to be easily extendable, allowing users to build upon the existing functionalities.
-*With this package, moving from Python to Julia for quantum physics simulations has never been easier*, due to the similar syntax and functionalities.
+*__With this package, moving from Python to Julia for quantum physics simulations has never been easier__*, due to the similar syntax and functionalities.
-## Features
-
-QuantumToolbox.jl is equipped with a robust set of features:
-
-- **Quantum State and Operator Manipulation:** Easily handle quantum states and operators with a rich set of tools, with the same functionalities as QuTiP.
-- **Dynamical Evolution:** Advanced solvers for time evolution of quantum systems, thanks to the powerful [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl) package.
-- **GPU Computing:** Leverage GPU resources for high-performance computing. For example, you run the master equation direclty on the GPU with the same syntax as the CPU case.
-- **Distributed Computing:** Distribute the computation over multiple nodes (e.g., a cluster). For example, you can run undreds of quantum trajectories in parallel on a cluster, with, again, the same syntax as the simple case.
-- **Easy Extension:** Easily extend the package, taking advantage of the Julia language features, like multiple dispatch and metaprogramming.
-
-## [Installation](@id doc:Installation)
+# [Installation](@id doc:Installation)
!!! note "Requirements"
`QuantumToolbox.jl` requires `Julia 1.10+`.
@@ -28,8 +69,8 @@ To install `QuantumToolbox.jl`, run the following commands inside Julia's intera
using Pkg
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
+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.10) pkg> add QuantumToolbox
```
More information about `Julia`'s package manager can be found at [`Pkg.jl`](https://julialang.github.io/Pkg.jl/v1/).
@@ -41,78 +82,29 @@ QuantumToolbox.versioninfo()
QuantumToolbox.about()
```
-## Brief Example
-
-We now provide a brief example to demonstrate the similarity between [QuantumToolbox.jl](https://github.com/qutip/QuantumToolbox.jl) and [QuTiP](https://github.com/qutip/qutip).
-
-Let's consider a quantum harmonic oscillator with a Hamiltonian given by:
-
-```math
-\hat{H} = \omega \hat{a}^\dagger \hat{a}
-```
-
-where ``\hat{a}`` and ``\hat{a}^\dagger`` are the annihilation and creation operators, respectively. We can define the Hamiltonian as follows:
-
-```julia
-using QuantumToolbox
-
-N = 20 # cutoff of the Hilbert space dimension
-ω = 1.0 # frequency of the harmonic oscillator
-
-a = destroy(N) # annihilation operator
-
-H = ω * a' * a
-```
-
-We now introduce some losses in a thermal environment, described by the Lindblad master equation:
-
-```math
-\frac{d \hat{\rho}}{dt} = -i [\hat{H}, \hat{\rho}] + \gamma \mathcal{D}[\hat{a}] \hat{\rho}
-```
-
-where ``\hat{\rho}`` is the density matrix, ``\gamma`` is the damping rate, and ``\mathcal{D}[\hat{a}]`` is the Lindblad dissipator, defined as:
-
-```math
-\mathcal{D}[\hat{a}]\hat{\rho} = \hat{a}\hat{\rho}\hat{a}^\dagger - \frac{1}{2}\hat{a}^\dagger\hat{a}\hat{\rho} - \frac{1}{2}\hat{\rho}\hat{a}^\dagger\hat{a}
-```
-
-We now compute the time evolution of the system using the [`mesolve`](@ref) function, starting from the initial state ``\ket{\psi (0)} = \ket{3}``:
-
-```julia
-γ = 0.1 # damping rate
-
-ψ0 = fock(N, 3) # initial state
-
-tlist = range(0, 10, 100) # time list
-
-c_ops = [sqrt(γ) * a]
-e_ops = [a' * a]
-
-sol = mesolve(H, ψ0, tlist, c_ops, e_ops = e_ops)
-```
-
-We can extract the expectation value of the number operator ``\hat{a}^\dagger \hat{a}`` with the command `sol.expect`, and the states with the command `sol.states`.
-
-### Support for GPU calculation
-
-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
-
-H_gpu = ω * a_gpu' * a_gpu
-
-ψ0_gpu = cu(fock(N, 3))
-
-c_ops = [sqrt(γ) * a_gpu]
-e_ops = [a_gpu' * a_gpu]
-
-sol = mesolve(H_gpu, ψ0_gpu, tlist, c_ops, e_ops = e_ops)
-```
+# [Other Useful Packages](@id doc:Other-Useful-Packages)
+
+In order to get a better experience and take full advantage of `QuantumToolbox`, we recommend the following external packages:
+
+- Standard `Julia` Libraries: (recommended to also `using` with `QuantumToolbox.jl`)
+ - [`LinearAlgebra.jl`](https://github.com/JuliaLang/LinearAlgebra.jl)
+ - [`SparseArrays.jl`](https://github.com/JuliaSparse/SparseArrays.jl)
+- Solver `alg`orithms:
+ - [`DifferentialEquations.jl`](https://github.com/SciML/DifferentialEquations.jl) or [`OrdinaryDiffEq.jl`](https://github.com/SciML/OrdinaryDiffEq.jl)
+ - [`LinearSolve.jl`](https://github.com/SciML/LinearSolve.jl)
+- GPU support:
+ - [`CUDA.jl`](https://github.com/JuliaGPU/CUDA.jl)
+- Distributed Computing support:
+ - [`Distributed.jl`](https://github.com/JuliaLang/Distributed.jl)
+ - [`SlurmClusterManager.jl`](https://github.com/JuliaParallel/SlurmClusterManager.jl)
+- Plotting Libraries:
+ - [`Makie.jl`](https://github.com/MakieOrg/Makie.jl)
+- Automatic Differentiation:
+ - [`SciMLSensitivity.jl`](https://github.com/SciML/SciMLSensitivity.jl)
+ - [`Zygote.jl`](https://github.com/FluxML/Zygote.jl)
+ - [`Enzyme.jl`](https://github.com/EnzymeAD/Enzyme.jl)
+ - [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl)
+- Packages for other advanced usage:
+ - [`StaticArrays.jl`](https://github.com/JuliaArrays/StaticArrays.jl)
+ - [`SciMLOperators.jl`](https://github.com/SciML/SciMLOperators.jl)
+ - [`DiffEqCallbacks.jl`](https://github.com/SciML/DiffEqCallbacks.jl)
diff --git a/docs/src/resources/acknowledgements.md b/docs/src/resources/acknowledgements.md
new file mode 100644
index 000000000..7f6161e73
--- /dev/null
+++ b/docs/src/resources/acknowledgements.md
@@ -0,0 +1,25 @@
+# [Acknowledgements](@id doc:Acknowledgements)
+
+## [Fundings](@id doc:Fundings)
+
+`QuantumToolbox.jl` is supported by the [Unitary Fund](https://unitary.fund), a grant program for quantum technology projects.
+
+```@raw html
+
+```
+
+## [Other Acknowledgements](@id doc:Other-Acknowledgements)
+
+We are also grateful to the [Zulip](https://zulip.com) team for providing a free chat service for open-source projects.
+
+```@raw html
+
+```
diff --git a/docs/src/api.md b/docs/src/resources/api.md
similarity index 56%
rename from docs/src/api.md
rename to docs/src/resources/api.md
index 20c35bd29..e4e5e901a 100644
--- a/docs/src/api.md
+++ b/docs/src/resources/api.md
@@ -1,35 +1,44 @@
```@meta
CurrentModule = QuantumToolbox
+
+DocTestSetup = quote
+ using LinearAlgebra
+ using SparseArrays
+ using QuantumToolbox
+end
```
# [API](@id doc-API)
-## Contents
+**Table of contents**
-```@contents
-Pages = ["api.md"]
-```
+[[toc]]
## [Quantum object (Qobj) and type](@id doc-API:Quantum-object-and-type)
```@docs
-BraQuantumObject
+Space
+EnrSpace
+Dimensions
+GeneralDimensions
+AbstractQuantumObject
Bra
-KetQuantumObject
Ket
-OperatorQuantumObject
Operator
-OperatorBraQuantumObject
OperatorBra
-OperatorKetQuantumObject
OperatorKet
-SuperOperatorQuantumObject
SuperOperator
QuantumObject
-OperatorSum
-size
-eltype
-length
+QuantumObjectEvolution
+Base.size
+Base.eltype
+Base.length
+SciMLOperators.cache_operator
+```
+
+## [Qobj boolean functions](@id doc-API:Qobj-boolean-functions)
+
+```@docs
isbra
isket
isoper
@@ -40,11 +49,15 @@ LinearAlgebra.ishermitian
LinearAlgebra.issymmetric
LinearAlgebra.isposdef
isunitary
+SciMLOperators.iscached
+SciMLOperators.isconstant
```
## [Qobj arithmetic and attributes](@id doc-API:Qobj-arithmetic-and-attributes)
```@docs
+Base.zero
+Base.one
Base.conj
LinearAlgebra.transpose
LinearAlgebra.adjoint
@@ -64,7 +77,7 @@ LinearAlgebra.diag
proj
ptrace
purity
-permute
+SparseArrays.permute
tidyup
tidyup!
get_data
@@ -91,8 +104,8 @@ ket2dm
expect
variance
LinearAlgebra.kron
-sparse_to_dense
-dense_to_sparse
+to_dense
+to_sparse
vec2mat
mat2vec
```
@@ -110,6 +123,8 @@ coherent_dm
thermal_dm
maximally_mixed_dm
rand_dm
+enr_fock
+enr_thermal_dm
spin_state
spin_coherent
bell_state
@@ -140,6 +155,8 @@ QuantumToolbox.momentum
phase
fdestroy
fcreate
+enr_destroy
+enr_identity
tunneling
qft
eye
@@ -154,69 +171,137 @@ lindblad_dissipator
## [Synonyms of functions for Qobj](@id doc-API:Synonyms-of-functions-for-Qobj)
```@docs
Qobj
+QobjEvo
shape
isherm
trans
dag
matrix_element
unit
+tensor
+⊗
+qeye
+vector_to_operator
+operator_to_vector
sqrtm
logm
expm
sinm
cosm
-tensor
-⊗
-qeye
+qeye_like
+qzero_like
```
## [Time evolution](@id doc-API:Time-evolution)
```@docs
+TimeEvolutionProblem
TimeEvolutionSol
TimeEvolutionMCSol
+TimeEvolutionStochasticSol
+average_states
+average_expect
+std_expect
sesolveProblem
mesolveProblem
-lr_mesolveProblem
mcsolveProblem
mcsolveEnsembleProblem
+ssesolveProblem
+ssesolveEnsembleProblem
+smesolveProblem
+smesolveEnsembleProblem
sesolve
mesolve
-lr_mesolve
mcsolve
+ssesolve
+smesolve
dfd_mesolve
-dsf_mesolve
-dsf_mcsolve
liouvillian
liouvillian_generalized
+bloch_redfield_tensor
+brterm
+brmesolve
+```
+
+### [Steady State Solvers](@id doc-API:Steady-State-Solvers)
+
+```@docs
steadystate
-steadystate_floquet
+steadystate_fourier
+SteadyStateDirectSolver
+SteadyStateEigenSolver
+SteadyStateLinearSolver
SteadyStateODESolver
+SSFloquetEffectiveLiouvillian
+```
+
+### [Dynamical Shifted Fock method](@id doc-API:Dynamical-Shifted-Fock-method)
+
+```@docs
+dsf_mesolve
+dsf_mcsolve
+```
+
+### [Low-rank time evolution](@id doc-API:Low-rank-time-evolution)
+
+```@docs
+TimeEvolutionLRSol
+lr_mesolveProblem
+lr_mesolve
```
## [Correlations and Spectrum](@id doc-API:Correlations-and-Spectrum)
```@docs
correlation_3op_2t
+correlation_3op_1t
correlation_2op_2t
correlation_2op_1t
+spectrum_correlation_fft
spectrum
+ExponentialSeries
+PseudoInverse
+Lanczos
```
-## [Metrics](@id doc-API:Metrics)
+## [Entropy and Metrics](@id doc-API:Entropy-and-Metrics)
```@docs
entropy_vn
+entropy_relative
+entropy_linear
+entropy_mutual
+entropy_conditional
entanglement
-tracedist
+concurrence
+negativity
fidelity
+tracedist
+hilbert_dist
+hellinger_dist
+bures_dist
+bures_angle
+```
+
+## [Spin Lattice](@id doc-API:Spin-Lattice)
+
+```@docs
+Lattice
+multisite_operator
+DissipativeIsing
+```
+
+## [Symmetries and Block Diagonalization](@id doc-API:Symmetries-and-Block-Diagonalization)
+
+```@docs
+block_diagonal_form
+BlockDiagonalForm
```
## [Miscellaneous](@id doc-API:Miscellaneous)
```@docs
wigner
-negativity
```
## [Linear Maps](@id doc-API:Linear-Maps)
@@ -228,16 +313,36 @@ AbstractLinearMap
## [Utility functions](@id doc-API:Utility-functions)
```@docs
+QuantumToolbox.settings
QuantumToolbox.versioninfo
QuantumToolbox.about
+QuantumToolbox.cite
gaussian
-n_th
+n_thermal
+PhysicalConstants
+convert_unit
row_major_reshape
meshgrid
-_calculate_expectation!
-_adjM_condition_variational
-_adjM_affect!
-_adjM_condition_ratio
-_pinv!
-dBdz!
+enr_state_dictionaries
+```
+
+## [Visualization](@id doc-API:Visualization)
+
+```@docs
+plot_wigner
+plot_fock_distribution
+```
+
+### [Bloch Sphere](@id doc-API:Bloch-Sphere)
+
+```@docs
+Bloch
+plot_bloch
+render
+add_points!
+add_vectors!
+add_line!
+add_arc!
+add_states!
+clear!
```
diff --git a/docs/src/resources/bibliography.bib b/docs/src/resources/bibliography.bib
new file mode 100644
index 000000000..9af3e514b
--- /dev/null
+++ b/docs/src/resources/bibliography.bib
@@ -0,0 +1,141 @@
+@book{Gardiner-Zoller2004,
+ title = {Quantum Noise},
+ ISBN = {9783540223016},
+ url = {https://link.springer.com/book/9783540223016},
+ publisher = {Springer Berlin, Heidelberg},
+ author = {Gardiner, Crispin and Zoller, Peter},
+ year = {2004},
+ month = aug
+}
+
+@book{Nielsen-Chuang2011,
+ title = {Quantum Computation and Quantum Information: 10th Anniversary Edition},
+ ISBN = {9780511976667},
+ DOI = {10.1017/cbo9780511976667},
+ publisher = {Cambridge University Press},
+ author = {Nielsen, Michael A. and Chuang, Isaac L.},
+ year = {2012},
+ month = jun
+}
+
+@article{Jozsa1994,
+ author = {Richard Jozsa},
+ title = {Fidelity for Mixed Quantum States},
+ journal = {Journal of Modern Optics},
+ volume = {41},
+ number = {12},
+ pages = {2315--2323},
+ year = {1994},
+ publisher = {Taylor \& Francis},
+ doi = {10.1080/09500349414552171}
+}
+
+@article{gravina2024adaptive,
+ title = {{Adaptive variational low-rank dynamics for open quantum systems}},
+ author = {Gravina, Luca and Savona, Vincenzo},
+ journal = {Phys. Rev. Res.},
+ volume = {6},
+ issue = {2},
+ pages = {023072},
+ numpages = {18},
+ year = {2024},
+ month = {Apr},
+ publisher = {American Physical Society},
+ doi = {10.1103/PhysRevResearch.6.023072}
+}
+
+@article{Tanimura1989,
+ title = {Time Evolution of a Quantum System in Contact with a Nearly Gaussian-Markoffian Noise Bath},
+ volume = {58},
+ ISSN = {1347-4073},
+ DOI = {10.1143/jpsj.58.101},
+ number = {1},
+ journal = {Journal of the Physical Society of Japan},
+ publisher = {Physical Society of Japan},
+ author = {Tanimura, Yoshitaka and Kubo, Ryogo},
+ year = {1989},
+ month = jan,
+ pages = {101–114}
+}
+
+@article{Huang2023,
+ doi = {10.1038/s42005-023-01427-2},
+ year = {2023},
+ month = {Oct},
+ publisher = {Nature Portfolio},
+ volume = {6},
+ number = {1},
+ pages = {313},
+ author = {Huang, Yi-Te and Kuo, Po-Chen and Lambert, Neill and Cirio, Mauro and Cross, Simon and Yang, Shen-Liang and Nori, Franco and Chen, Yueh-Nan},
+ title = {An efficient {J}ulia framework for hierarchical equations of motion in open quantum systems},
+ journal = {Communications Physics}
+}
+
+@book{Wiseman2009Quantum,
+ title={Quantum Measurement and Control},
+ ISBN={9781107424159},
+ url={http://dx.doi.org/10.1017/CBO9780511813948},
+ DOI={10.1017/cbo9780511813948},
+ publisher={Cambridge University Press},
+ author={Wiseman, Howard M. and Milburn, Gerard J.},
+ year={2009},
+ month=nov
+}
+
+@article{Vedral-Plenio1998,
+ title = {Entanglement measures and purification procedures},
+ author = {Vedral, V. and Plenio, M. B.},
+ journal = {Phys. Rev. A},
+ volume = {57},
+ issue = {3},
+ pages = {1619--1633},
+ numpages = {0},
+ year = {1998},
+ month = {Mar},
+ publisher = {American Physical Society},
+ doi = {10.1103/PhysRevA.57.1619},
+ url = {https://link.aps.org/doi/10.1103/PhysRevA.57.1619}
+}
+
+@article{Spehner2017,
+ title={Geometric measures of quantum correlations with Bures and Hellinger distances},
+ author={D. Spehner and F. Illuminati and M. Orszag and W. Roga},
+ year={2017},
+ journal={arXiv:1611.03449},
+ url={https://arxiv.org/abs/1611.03449},
+}
+
+@article{Hill-Wootters1997,
+ title = {Entanglement of a Pair of Quantum Bits},
+ author = {Hill, Sam A. and Wootters, William K.},
+ journal = {Phys. Rev. Lett.},
+ volume = {78},
+ issue = {26},
+ pages = {5022--5025},
+ numpages = {0},
+ year = {1997},
+ month = {Jun},
+ publisher = {American Physical Society},
+ doi = {10.1103/PhysRevLett.78.5022},
+ url = {https://link.aps.org/doi/10.1103/PhysRevLett.78.5022}
+}
+
+@book{Cohen_Tannoudji_atomphoton,
+ address = {New York},
+ author = {{Cohen-Tannoudji}, C. and {Grynberg}, G. and {Dupont-Roc}, J.},
+ publisher = {Wiley},
+ timestamp = {2010-12-01T16:20:40.000+0100},
+ title = {Atom-Photon Interactions: Basic Processes and Applications },
+ year = 1992
+}
+
+@book{breuer2002,
+ title = {The Theory of Open Quantum Systems},
+ author = {Breuer, Heinz-Peter and Petruccione, Francesco},
+ year = {2002},
+ publisher = {Oxford university press},
+ address = {Oxford New York},
+ isbn = {978-0-19-852063-4},
+ langid = {english},
+ lccn = {530.12}
+}
diff --git a/docs/src/resources/bibliography.md b/docs/src/resources/bibliography.md
new file mode 100644
index 000000000..bbb721388
--- /dev/null
+++ b/docs/src/resources/bibliography.md
@@ -0,0 +1,10 @@
+# [Bibliography](@id doc-Bibliography)
+
+```@meta
+CurrentModule = QuantumToolbox
+```
+
+This page is generated by [`DocumenterCitations.jl` with author-year style](https://juliadocs.org/DocumenterCitations.jl/stable/gallery/#author_year_style).
+
+```@bibliography
+```
diff --git a/docs/src/resources/contributing.md b/docs/src/resources/contributing.md
new file mode 100644
index 000000000..5518c6998
--- /dev/null
+++ b/docs/src/resources/contributing.md
@@ -0,0 +1,106 @@
+# [Contributing to Quantum Toolbox in Julia](@id doc-Contribute)
+
+## [Quick Start](@id doc-Contribute:Quick-Start)
+
+`QuantumToolbox.jl` is developed using the [`git`](https://git-scm.com/) version-control system, with the [main repository](https://github.com/qutip/QuantumToolbox.jl) hosted in the [qutip organisation on GitHub](https://github.com/qutip). You will need to be familiar with [`git`](https://git-scm.com/) as a tool, and the [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow) workflow for branching and making pull requests. The exact details of environment set-up, build process, and runtests vary by repository are discussed below. In overview, the steps to contribute are:
+
+- Consider creating an issue on the GitHub page of the relevant repository, describing the change you think should be made and why, so we can discuss details with you and make sure it is appropriate.
+- *__If this is your first contribution__*, make a fork of the relevant repository on GitHub (which will be called as `origin`) and clone it to your local computer. Also add our copy as a remote (let's call it `qutip` here): `git remote add qutip https://github.com/qutip/`.
+- Start from the `main` branch in your local computer (`git checkout main`), and pull all the changes from the remote (`qutip`) repository (on GitHub) to make sure you have an up-to-date copy: `git pull qutip main`.
+- Switch to a new `git` branch in your local computer: `git checkout -b `.
+- Make the changes you want.
+- Go through the following build processes (if the changes you made relates to any of them) locally in your computer to build the final result so you can check your changes work sensibly:
+ - Write and make sure all the runtests pass. See [here](@ref doc-Contribute:Runtests) for more details.
+ - Make sure all the changes match the `Julia` code format (style). See [here](@ref doc-Contribute:Julia-Code-Format) for more details.
+ - Improve and make sure the documentation can be built successfully. See [here](@ref doc-Contribute:Documentation) for more details.
+ - Update changelog. See [here](@ref doc-Contribute:Update-ChangeLog) for more details.
+- Add the changed files to the `git` staging area `git add ...`, and then create some commits with short-but-descriptive names: `git commit`.
+- Push the changes to your fork (`origin`): `git push -u origin `. You won’t be able to push to the remote (`qutip`) repositories directly.
+- Go to the GitHub website for the repository you are contributing to, click on the “Pull Requests” tab, click the “New Pull Request” button, and follow the instructions there.
+
+Once the pull request (PR) is created, some members of the QuTiP admin team will review the code to make sure it is suitable for inclusion in the library, to check the programming, and to ensure everything meets our standards. For some repositories, several automated CI pipelines will run whenever you create or modify a PR. In general, these will basically be the same ones which you can run locally, and all CI pipelines are required to pass online before your changes are merged to the remote `main` branch. There might be some feedbacks and requested changes. You can add more commits to address these, and push them to the branch (``) of your fork (`origin`) to update the PR.
+
+The rest of this document covers programming standards.
+
+## [Runtests](@id doc-Contribute:Runtests)
+
+All the test scripts should be located in the folder `test` in the repository. To run the test, use the following command under the *__root directory of the repository__* you are working on:
+
+```shell
+make test
+```
+
+This command will automatically rebuild `Julia` and run the script located in `test/runtests.jl` (should cover both the original tests and the new test(s) you add).
+
+The tests are divided into several test groups, where the group names are defined in the file `test/runtests.jl` with a variable `GROUP`. One can also run the test scripts just for a certain test group by adding an argument `GROUP=` to the `make test` command. For example, to run the tests for group `Core`, one can use the following command:
+
+```shell
+make GROUP=Core test
+```
+
+### [Test Item Framework for Core tests](@id doc-Contribute:Test-Item-Framework-for-Core-tests)
+
+The tests in `GROUP=Core` are provided using the [Test Item Framework](https://www.julia-vscode.org/docs/stable/userguide/testitems/), which structures the test codes into `@testitems` and makes it easier to run individually.
+
+The [VS Code](https://code.visualstudio.com/) and its [Julia extension](https://www.julia-vscode.org/) provides us with options to run individual `@testitems`. It is much easier to find the specific core test that failed since the [Julia extension](https://www.julia-vscode.org/) in [VS Code](https://code.visualstudio.com/) will collect all core test failures and then display them in a structured way, directly at the place in the code where a specific core test failed. See [here](https://www.julia-vscode.org/docs/stable/userguide/testitems/) for more details.
+
+## [Julia Code Format](@id doc-Contribute:Julia-Code-Format)
+
+We use [`JuliaFormatter.jl`](https://github.com/domluna/JuliaFormatter.jl) to format all the source codes. The code style and extra formatting options is defined in the file `.JuliaFormatter.toml` in the repository.
+
+To format the changed codes, use the following command under the *__root directory of the repository__* you are working on:
+
+!!! note "Requirements"
+ If this is your first time running `make` command in the local repository you are working on or you just had reinstalled `Julia`, you should run `make setup` first.
+
+```shell
+make format
+```
+
+## [Documentation](@id doc-Contribute:Documentation)
+
+All the documentation source files [in markdown (`.md`) format] and build scripts should be located in the folder `docs` in the repository.
+
+The document pages will be generated in the folder `docs/build/1/` (which is ignored by `git`) in the repository.
+
+To instantiate and build the documentation, run the following command under the *__root directory of the repository__* you are working on:
+
+!!! note "Requirements"
+ You need to install `Node.js` and `npm` first.
+
+```shell
+make docs
+```
+
+This command will automatically rebuild `Julia` and run the script located in `docs/make.jl` (should be able to build the necessary files for the documentation).
+
+To read the documentation in a browser, you can run the following command:
+
+```shell
+make vitepress
+```
+
+This will start a local Vitepress site of documentation at `http://localhost:5173` in your computer.
+
+## [Update ChangeLog](@id doc-Contribute:Update-ChangeLog)
+
+The changelog is written in the file `CHANGELOG.md` in the repository. If you add some changes to the repository and made a PR, you should also add some messages or release notes together with the related PRs/issues entries to `CHANGELOG.md`. For example, add a new line in `CHANGELOG.md`:
+
+```markdown
+- some messages to describe the changes. ([#issue-ID], [#PR-ID])
+```
+
+See also the [ChangeLog page](@ref ChangeLog) for more examples.
+
+After that, you can run the following command under the *__root directory of the repository__* you are working on:
+
+!!! note "Requirements"
+ If this is your first time running `make` command in the local repository you are working on or you just had reinstalled `Julia`, you should run `make setup` first.
+
+```shell
+make changelog
+```
+
+This will automatically generate the full URLs for the references to PRs/issues by utilizing [`Changelog.jl`](https://github.com/JuliaDocs/Changelog.jl).
+
+If the changes you made are not necessary to be recorded in `CHANGELOG.md`, you can add the label `[Skip ChangeLog]` to the PR you made in the GitHub repository.
diff --git a/docs/src/tutorials/lowrank.md b/docs/src/tutorials/lowrank.md
deleted file mode 100644
index 42a100bd0..000000000
--- a/docs/src/tutorials/lowrank.md
+++ /dev/null
@@ -1,166 +0,0 @@
-# [Low rank master equation](@id doc-tutor:Low-rank-master-equation)
-
-We start by importing the packages
-
-```@example lowrank
-using QuantumToolbox
-using CairoMakie
-CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
-```
-
-Define lattice
-
-```@example lowrank
-Nx, Ny = 2, 3
-latt = Lattice(Nx = Nx, Ny = Ny)
-```
-
-Define lr-space dimensions
-
-```@example lowrank
-N_cut = 2 # Number of states of each mode
-N_modes = latt.N # Number of modes
-N = N_cut^N_modes # Total number of states
-M = Nx * Ny + 1 # Number of states in the LR basis
-```
-
-Define lr states. Take as initial state all spins up. All other N states are taken as those with miniman Hamming distance to the initial state.
-
-```@example lowrank
-ϕ = Vector{QuantumObject{Vector{ComplexF64},KetQuantumObject}}(undef, M)
-ϕ[1] = kron(repeat([basis(2, 0)], N_modes)...)
-
-global i = 1
-for j in 1:N_modes
- global i += 1
- i <= M && (ϕ[i] = mb(sp, j, latt) * ϕ[1])
-end
-for k in 1:N_modes-1
- for l in k+1:N_modes
- global i += 1
- i <= M && (ϕ[i] = mb(sp, k, latt) * mb(sp, l, latt) * ϕ[1])
- end
-end
-for i in i+1:M
- ϕ[i] = QuantumObject(rand(ComplexF64, size(ϕ[1])[1]), dims = ϕ[1].dims)
- normalize!(ϕ[i])
-end
-```
-
-Define the initial state
-
-```@example lowrank
-z = hcat(broadcast(x -> x.data, ϕ)...)
-p0 = 0.0 # Population of the lr states other than the initial state
-B = Matrix(Diagonal([1 + 0im; p0 * ones(M - 1)]))
-S = z' * z # Overlap matrix
-B = B / tr(S * B) # Normalize B
-
-ρ = QuantumObject(z * B * z', dims = ones(Int, N_modes) * N_cut); # Full density matrix
-```
-
-Define the Hamiltonian and collapse operators
-
-```@example lowrank
-# Define Hamiltonian and collapse operators
-Jx = 0.9
-Jy = 1.04
-Jz = 1.0
-hx = 0.0
-γ = 1
-
-Sx = sum([mb(sx, i, latt) for i in 1:latt.N])
-Sy = sum([mb(sy, i, latt) for i in 1:latt.N])
-Sz = sum([mb(sz, i, latt) for i in 1:latt.N])
-SFxx = sum([mb(sx, i, latt) * mb(sx, j, latt) for i in 1:latt.N for j in 1:latt.N])
-
-H, c_ops = TFIM(Jx, Jy, Jz, hx, γ, latt; bc = pbc, order = 1)
-e_ops = (Sx, Sy, Sz, SFxx)
-
-tl = LinRange(0, 10, 100);
-```
-
-### Full evolution
-
-```@example lowrank
-@time mesol = mesolve(H, ρ, tl, c_ops; e_ops = [e_ops...]);
-A = Matrix(mesol.states[end].data)
-λ = eigvals(Hermitian(A))
-Strue = -sum(λ .* log2.(λ)) / latt.N;
-```
-
-### Low Rank evolution
-
-Define functions to be evaluated during the low-rank evolution
-
-```@example lowrank
-function f_purity(p, z, B)
- N = p.N
- M = p.M
- S = p.S
- T = p.temp_MM
-
- mul!(T, S, B)
- return tr(T^2)
-end
-
-function f_trace(p, z, B)
- N = p.N
- M = p.M
- S = p.S
- T = p.temp_MM
-
- mul!(T, S, B)
- return tr(T)
-end
-
-function f_entropy(p, z, B)
- C = p.A0
- σ = p.Bi
-
- mul!(C, z, sqrt(B))
- mul!(σ, C', C)
- λ = eigvals(Hermitian(σ))
- λ = λ[λ.>1e-10]
- return -sum(λ .* log2.(λ))
-end;
-```
-
-Define the options for the low-rank evolution
-
-```@example lowrank
-opt =
- 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);
-```
-
-Plot the results
-
-```@example lowrank
-m_me = real(mesol.expect[3, :]) / Nx / Ny
-m_lr = real(lrsol.expvals[3, :]) / Nx / Ny
-
-fig = Figure(size = (800, 400), fontsize = 15)
-ax = Axis(fig[1, 1], xlabel = L"\gamma t", ylabel = L"M_{z}", xlabelsize = 20, ylabelsize = 20)
-lines!(ax, tl, m_lr, label = L"LR $[M=M(t)]$", linewidth = 2)
-lines!(ax, tl, m_me, label = "Fock", linewidth = 2, linestyle = :dash)
-axislegend(ax, position = :rb)
-
-ax2 = Axis(fig[1, 2], xlabel = L"\gamma t", ylabel = "Value", xlabelsize = 20, ylabelsize = 20)
-lines!(ax2, tl, 1 .- real(lrsol.funvals[1, :]), label = L"$1-P$", linewidth = 2)
-lines!(
- ax2,
- tl,
- 1 .- real(lrsol.funvals[3, :]),
- label = L"$1-\mathrm{Tr}(\rho)$",
- linewidth = 2,
- linestyle = :dash,
- color = :orange,
-)
-lines!(ax2, tl, real(lrsol.funvals[2, :]) / Nx / Ny, color = :blue, label = L"S", linewidth = 2)
-hlines!(ax2, [Strue], color = :blue, linestyle = :dash, linewidth = 2, label = L"S^{\,\mathrm{true}}_{\mathrm{ss}}")
-axislegend(ax2, position = :rb)
-
-fig
-```
diff --git a/docs/src/users_guide/HEOM.md b/docs/src/users_guide/HEOM.md
new file mode 100644
index 000000000..9b33a4a5e
--- /dev/null
+++ b/docs/src/users_guide/HEOM.md
@@ -0,0 +1,33 @@
+# [Hierarchical Equations of Motion](@id doc:Hierarchical-Equations-of-Motion)
+
+The hierarchical equations of motion (HEOM) approach was originally developed by [Tanimura1989](@citet) in the context of physical chemistry to "exactly" solve a quantum system (labeled as ``\textrm{s}``) in contact with a bosonic environment, encapsulated in the following total Hamiltonian:
+
+```math
+\hat{H}_{\textrm{total}} = \hat{H}_{\textrm{s}} + \sum_k \omega_k \hat{b}^\dagger_k \hat{b}_k + \hat{V}_{\textrm{s}} \sum_k g_k \left(\hat{b}_k + \hat{b}^\dagger_k\right),
+```
+
+where ``\hat{b}_k`` (``\hat{b}^\dagger_k``) is the bosonic annihilation (creation) operator associated to the ``k``th mode (with frequency ``\omega_k``), ``\hat{V}_{\textrm{s}}`` refer to the coupling operator acting on the system's degree of freedom, and ``g_k`` are the coupling strengths.
+
+As in other solutions to this problem, the properties of the bath are encapsulated by its temperature and its spectral density,
+
+```math
+J(\omega) = 2 \pi \sum_k g^2_k \delta(\omega - \omega_k).
+```
+
+In the HEOM approach, for bosonic baths, one typically chooses a Drude-Lorentz spectral density:
+
+```math
+J_{\textrm{DL}}(\omega) = \frac{4 \Delta W \omega}{\omega^2 + W^2},
+```
+
+or an under-damped Brownian motion spectral density,
+
+```math
+J_{\textrm{U}}(\omega)=\frac{2 \Delta^2 W \omega}{(\omega^2 - \omega_0^2)^2 + \omega^2 W^2}.
+```
+
+Here, ``\Delta`` represents the coupling strength between the system and the bosonic bath with band-width ``W`` and resonance frequency ``\omega_0``.
+
+We introduce an efficient `Julia` framework for HEOM approach called [`HierarchicalEOM.jl`](https://github.com/qutip/HierarchicalEOM.jl). This package is built upon `QuantumToolbox.jl` and provides a user-friendly and efficient tool to simulate complex open quantum systems based on HEOM approach. For a detailed explanation of this package, we recommend to read its [documentation](https://qutip.org/HierarchicalEOM.jl/) and also the article [Huang2023](@citet).
+
+Given the spectral density, the HEOM approach requires a decomposition of the bath correlation functions in terms of exponentials. In the [documentation of `HierarchicalEOM.jl`](https://qutip.org/HierarchicalEOM.jl/), we not only describe how this is done for both bosonic and fermionic environments with code examples, but also describe how to solve the time evolution (dynamics), steady-states, and spectra based on HEOM approach.
diff --git a/docs/src/users_guide/QuantumObject/QuantumObject.md b/docs/src/users_guide/QuantumObject/QuantumObject.md
index 61e9569ff..4abd95e88 100644
--- a/docs/src/users_guide/QuantumObject/QuantumObject.md
+++ b/docs/src/users_guide/QuantumObject/QuantumObject.md
@@ -14,6 +14,9 @@ The key difference between classical and quantum mechanics is the use of operato
- `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
@@ -42,6 +45,8 @@ Qobj(rand(4, 4))
M = rand(ComplexF64, 4, 4)
Qobj(M, dims = [2, 2]) # dims as Vector
Qobj(M, dims = (2, 2)) # dims as Tuple (recommended)
+
+import QuantumToolbox: SVector # or using StaticArrays
Qobj(M, dims = SVector(2, 2)) # dims as StaticArrays.SVector (recommended)
```
@@ -49,11 +54,11 @@ Qobj(M, dims = SVector(2, 2)) # dims as StaticArrays.SVector (recommended)
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)
+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) for more information.
+ 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
@@ -61,14 +66,16 @@ Manually specifying the data for each quantum object is inefficient. Even more s
### 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
+- [`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
+- [`enr_fock`](@ref): Fock state in the excitation number restricted (ENR) space
+- [`enr_thermal_dm`](@ref): thermal state in the excitation number restricted (ENR) space
- [`spin_state`](@ref): spin state
- [`spin_coherent`](@ref): coherent spin state
- [`bell_state`](@ref): Bell state
@@ -103,6 +110,8 @@ Manually specifying the data for each quantum object is inefficient. Even more s
- [`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
+- [`enr_destroy`](@ref): destruction operator in the excitation number restricted (ENR) space
+- [`enr_identity`](@ref): identity operator in the excitation number restricted (ENR) space
- [`commutator`](@ref): commutator or anti-commutator
- [`tunneling`](@ref): tunneling operator
- [`qft`](@ref): discrete quantum Fourier transform matrix
@@ -171,6 +180,7 @@ println(isoper(a)) # operator
println(isoperket(a)) # operator-ket
println(isoperbra(a)) # operator-bra
println(issuper(a)) # super operator
+println(isconstant(a)) # time-independent or not
println(ishermitian(a)) # Hermitian
println(isherm(a)) # synonym of ishermitian(a)
println(issymmetric(a)) # symmetric
@@ -191,6 +201,8 @@ Vector{Int64}(v_d)
```
```@example Qobj
+using SparseArrays
+
v_s = SparseVector(v_d)
```
@@ -210,14 +222,14 @@ SparseMatrixCSC{Int64}(x_s)
Matrix{Float64}(x_s)
```
-To convert between dense and sparse arrays, one can also use [`dense_to_sparse`](@ref) and [`sparse_to_dense`](@ref):
+To convert between dense and sparse arrays, one can also use [`to_sparse`](@ref) and [`to_dense`](@ref):
```@example Qobj
-x_d = sparse_to_dense(x_s)
+x_d = to_dense(x_s)
```
```@example Qobj
-dense_to_sparse(x_d)
+to_sparse(x_d)
```
!!! note "Convert to GPU arrays"
diff --git a/docs/src/users_guide/QuantumObject/QuantumObject_functions.md b/docs/src/users_guide/QuantumObject/QuantumObject_functions.md
index 698017dce..3a1b16da5 100644
--- a/docs/src/users_guide/QuantumObject/QuantumObject_functions.md
+++ b/docs/src/users_guide/QuantumObject/QuantumObject_functions.md
@@ -10,6 +10,8 @@ Here is a table that summarizes all the supported linear algebra functions and a
| **Description** | **Function call** | **Synonyms** |
|:----------------|:------------------|:-------------|
+| zero-like array | [`zero(Q)`](@ref zero) | [`qzero_like(Q)`](@ref qzero_like) |
+| identity-like matrix | [`one(Q)`](@ref one) | [`qeye_like(Q)`](@ref qeye_like) |
| 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) |
@@ -41,8 +43,6 @@ Here is a table that summarizes all the supported linear algebra functions and a
- [`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)
diff --git a/docs/src/users_guide/autodiff.md b/docs/src/users_guide/autodiff.md
new file mode 100644
index 000000000..058b38cd2
--- /dev/null
+++ b/docs/src/users_guide/autodiff.md
@@ -0,0 +1,195 @@
+# [Automatic Differentiation](@id doc:autodiff)
+
+Automatic differentiation (AD) has emerged as a key technique in computational science, enabling exact and efficient computation of derivatives for functions defined by code. Unlike symbolic differentiation, which may produce complex and inefficient expressions, or finite-difference methods, which suffer from numerical instability and poor scalability, AD leverages the chain rule at the level of elementary operations to provide machine-precision gradients with minimal overhead.
+
+In `QuantumToolbox.jl`, we have introduced preliminary support for automatic differentiation. Many of the core functions are compatible with AD engines such as [`Zygote.jl`](https://github.com/FluxML/Zygote.jl), [`Enzyme.jl`](https://github.com/EnzymeAD/Enzyme.jl) or [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl), allowing users to compute gradients of observables or cost functionals involving the time evolution of open quantum systems. Although `QuantumToolbox.jl` was not originally designed with AD in mind, its architecture—rooted in Julia’s multiple dispatch and generic programming model—facilitated the integration of AD capabilities. Many core functions were already compatible with AD engines out of the box.
+
+!!! warning "Experimental Functionality"
+ At present, this functionality is considered experimental and not all parts of the library are AD-compatible. Here we provide a brief overview of the current state of AD support in `QuantumToolbox.jl` and how to use it.
+
+
+## [Forward versus Reverse Mode AD](@id doc:autodiff:forward-versus-reverse)
+
+Automatic differentiation can be broadly categorized into two modes: forward mode and reverse mode. The choice between these modes depends on the nature of the function being differentiated and the number of inputs and outputs:
+
+- **Forward Mode AD**: This mode is particularly efficient for functions with many outputs and few inputs. It works by propagating derivatives from the inputs through the computational graph to the outputs. Forward mode is often preferred when the number of input variables is small, as it computes the derivative of each output with respect to each input in a single pass.
+
+- **Reverse Mode AD**: In contrast, reverse mode is more efficient for functions with many inputs and few outputs. It operates by first computing the function's output and then propagating derivatives backward through the computational graph. This mode is commonly used in machine learning and optimization applications, where the loss function (output) depends on a large number of parameters (inputs).
+
+Understanding the differences between these two modes can help users choose the most appropriate approach for their specific use case in `QuantumToolbox.jl`.
+
+## [Differentiate the master equation](@id doc:autodiff:master-equation)
+
+One of the primary use cases for automatic differentiation in `QuantumToolbox.jl` is the differentiation of the master equation. The master equation describes the time evolution of a quantum system's density matrix under the influence of non-unitary dynamics, such as dissipation and decoherence. Let's consider a set of parameters $\mathbf{p} = (p_1, p_2, \ldots, p_n)$ that influence the system's dynamics. The Hamiltonian and the dissipators will depend on these parameters
+
+```math
+\hat{H} = \hat{H}(\mathbf{p}), \qquad \hat{L}_j = \hat{L}_j(\mathbf{p}),
+```
+
+Hence, the density matrix will evolve according to the master equation
+
+```@raw html
+
+```
+```math
+\begin{align}
+\frac{d \hat{\rho}(\mathbf{p}, t)}{dt} =& -i[\hat{H}(\mathbf{p}), \hat{\rho}(\mathbf{p}, t)] \\
+&+ \sum_j \hat{L}_j(\mathbf{p}) \hat{\rho}(\mathbf{p}, t) \hat{L}_j(\mathbf{p})^\dagger - \frac{1}{2} \left\{ \hat{L}_j(\mathbf{p})^\dagger \hat{L}_j(\mathbf{p}), \hat{\rho}(\mathbf{p}, t) \right\} \, ,
+\end{align} \tag{1}
+```
+
+which depends on the parameters $\mathbf{p}$ and time $t$.
+
+We now want to compute the expectation value of an observable $\hat{O}$ at time $t$:
+
+```math
+\langle \hat{O}(\mathbf{p}, t) \rangle = \text{Tr}[\hat{O} \hat{\rho}(\mathbf{p}, t)] \, ,
+```
+
+which will also depend on the parameters $\mathbf{p}$ and time $t$.
+
+Our goal is to compute the derivative of the expectation value with respect to the parameters:
+
+```math
+\frac{\partial \langle \hat{O}(\mathbf{p}, t) \rangle}{\partial p_j} = \frac{\partial}{\partial p_j} \text{Tr}[\hat{O} \hat{\rho}(\mathbf{p}, t)] \, ,
+```
+
+and to achieve this, we can use an AD engine like [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) (forward mode) or [`Zygote.jl`](https://github.com/FluxML/Zygote.jl) (reverse mode).
+
+Let's apply this to a simple example of a driven-dissipative quantum harmonic oscillator. The Hamiltonian in the drive frame is given by
+
+```math
+\hat{H} = \Delta \hat{a}^\dagger \hat{a} + F \left( \hat{a} + \hat{a}^\dagger \right) \, ,
+```
+
+where $\Delta = \omega_0 - \omega_d$ is the cavity-drive detuning, $F$ is the drive strength, and $\hat{a}$ and $\hat{a}^\dagger$ are the annihilation and creation operators, respectively. The system is subject to a single dissipative channel with a Lindblad operator $\hat{L} = \sqrt{\gamma} \hat{a}$, where $\gamma$ is the dissipation rate. If we start from the ground state $\hat{\rho}(0) = \vert 0 \rangle \langle 0 \vert$, the systems evolves according to the master equation in [Eq. (1)](#eq:master-equation).
+
+We now want to study the number of photons at the steady state, and how it varies with $\mathbf{p} = (\Delta, F, \gamma)$, namely $\nabla_\mathbf{p} \langle \hat{a}^\dagger \hat{a} \rangle (\mathbf{p}, t \to \infty)$. We can extract an analytical expression, in order to verify the correctness of the AD implementation:
+
+```math
+\langle \hat{a}^\dagger \hat{a} \rangle_\mathrm{ss} = \frac{F^2}{\Delta^2 + \frac{\gamma^2}{4}} \, ,
+```
+
+with the gradient given by
+
+```math
+\nabla_\mathbf{p} \langle \hat{a}^\dagger \hat{a} \rangle_\mathrm{ss} =
+\begin{pmatrix}
+\frac{-2 F^2 \Delta}{(\Delta^2 + \frac{\gamma^2}{4})^2} \\
+\frac{2 F}{\Delta^2 + \frac{\gamma^2}{4}} \\
+\frac{-F^2 \gamma}{2 (\Delta^2 + \frac{\gamma^2}{4})^2}
+\end{pmatrix} \, .
+```
+
+Although `QuantumToolbox.jl` has the [`steadystate`](@ref) function to directly compute the steady state without explicitly solving the master equation, here we use the [`mesolve`](@ref) function to integrate up to a long time $t_\mathrm{max}$, and then compute the expectation value of the number operator. We will demonstrate how to compute the gradient using both [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) and [`Zygote.jl`](https://github.com/FluxML/Zygote.jl).
+
+### [Forward Mode AD with ForwardDiff.jl](@id doc:autodiff:forward)
+
+```@setup autodiff
+using QuantumToolbox
+```
+
+We start by importing [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) and defining the parameters and operators:
+
+```@example autodiff
+using ForwardDiff
+
+const N = 20
+const a = destroy(N)
+const ψ0 = fock(N, 0)
+const t_max = 40
+const tlist = range(0, t_max, 100)
+```
+
+Then, we define a function that take the parameters `p` as an input and returns the expectation value of the number operator at `t_max`. We also define the analytical solution of the steady state photon number and its gradient for comparison:
+
+```@example autodiff
+function my_f_mesolve_direct(p)
+ H = p[1] * a' * a + p[2] * (a + a')
+ c_ops = [sqrt(p[3]) * a]
+ sol = mesolve(H, ψ0, tlist, c_ops, progress_bar = Val(false))
+ return real(expect(a' * a, sol.states[end]))
+end
+
+# Analytical solution
+function my_f_analytical(p)
+ Δ, F, γ = p
+ return F^2 / (Δ^2 + γ^2 / 4)
+end
+function my_grad_analytical(p)
+ Δ, F, γ = p
+ return [
+ -2 * F^2 * Δ / (Δ^2 + γ^2 / 4)^2,
+ 2 * F / (Δ^2 + γ^2 / 4),
+ -F^2 * γ / (2 * (Δ^2 + γ^2 / 4)^2)
+ ]
+end
+```
+
+The gradient can be computed using `ForwardDiff.gradient`:
+
+```@example autodiff
+Δ = 1.5
+F = 1.5
+γ = 1.5
+params = [Δ, F, γ]
+
+grad_exact = my_grad_analytical(params)
+grad_fd = ForwardDiff.gradient(my_f_mesolve_direct, params)
+```
+
+and test if the results match:
+
+```@example autodiff
+isapprox(grad_exact, grad_fd; atol = 1e-5)
+```
+
+### [Reverse Mode AD with Zygote.jl](@id doc:autodiff:reverse)
+
+Reverse-mode differentiation is significantly more challenging than forward-mode when dealing ODEs, as the complexity arises from the need to propagate gradients backward through the entire time evolution of the quantum state.
+
+`QuantumToolbox.jl` leverages the advanced capabilities of [`SciMLSensitivity.jl`](https://github.com/SciML/SciMLSensitivity.jl) to handle this complexity. [`SciMLSensitivity.jl`](https://github.com/SciML/SciMLSensitivity.jl) implements sophisticated methods for computing gradients of ODE solutions, such as the adjoint method, which computes gradients by solving an additional "adjoint" ODE backward in time. For more details on the adjoint method and other sensitivity analysis techniques, please refer to the [`SciMLSensitivity.jl` documentation](https://docs.sciml.ai/SciMLSensitivity/stable/).
+
+In order to reverse-differentiate the master equation, we need to define the operators as [`QuantumObjectEvolution`](@ref) objects, which use [`SciMLOperators.jl`](https://github.com/SciML/SciMLOperators.jl) to represent parameter-dependent operators.
+
+```@example autodiff
+using Zygote
+using SciMLSensitivity
+
+# For SciMLSensitivity.jl
+coef_Δ(p, t) = p[1]
+coef_F(p, t) = p[2]
+coef_γ(p, t) = sqrt(p[3])
+H = QobjEvo(a' * a, coef_Δ) + QobjEvo(a + a', coef_F)
+c_ops = [QobjEvo(a, coef_γ)]
+const L = liouvillian(H, c_ops)
+
+function my_f_mesolve(p)
+ sol = mesolve(
+ L,
+ ψ0,
+ tlist,
+ progress_bar = Val(false),
+ params = p,
+ sensealg = BacksolveAdjoint(autojacvec = EnzymeVJP()),
+ )
+
+ return real(expect(a' * a, sol.states[end]))
+end
+```
+
+And the gradient can be computed using `Zygote.gradient`:
+
+```@example autodiff
+grad_zygote = Zygote.gradient(my_f_mesolve, params)[1]
+```
+
+Finally, we can compare the results from [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) and [`Zygote.jl`](https://github.com/FluxML/Zygote.jl):
+
+```@example autodiff
+isapprox(grad_fd, grad_zygote; atol = 1e-5)
+```
+
+## [Conclusion](@id doc:autodiff:conclusion)
+
+In this section, we have explored the integration of automatic differentiation into `QuantumToolbox.jl`, enabling users to compute gradients of observables and cost functionals involving the time evolution of open quantum systems. We demonstrated how to differentiate the master equation using both forward mode with [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) and reverse mode with [`Zygote.jl`](https://github.com/FluxML/Zygote.jl), showcasing the flexibility and power of automatic differentiation in quantum computing applications. AD can be applied to other functions in `QuantumToolbox.jl`, although the support is still experimental and not all functions are guaranteed to be compatible. We encourage users to experiment with AD in their quantum simulations and contribute to the ongoing development of this feature.
\ No newline at end of file
diff --git a/docs/src/users_guide/cluster.md b/docs/src/users_guide/cluster.md
new file mode 100644
index 000000000..d36259f03
--- /dev/null
+++ b/docs/src/users_guide/cluster.md
@@ -0,0 +1,264 @@
+# [Intensive parallelization on a Cluster](@id doc:Intensive-parallelization-on-a-Cluster)
+
+## Introduction
+
+In this example, we will demonstrate how to seamlessly perform intensive parallelization on a cluster using the **QuantumToolbox.jl** package. Indeed, thanks to the [**Distributed.jl**](https://docs.julialang.org/en/v1/manual/distributed-computing/) and [**ClusterManagers.jl**](https://github.com/JuliaParallel/ClusterManagers.jl) packages, it is possible to parallelize on a cluster with minimal effort. The following examples are applied to a cluster with the [SLURM](https://slurm.schedmd.com/documentation.html) workload manager, but the same principles can be applied to other workload managers, as the [**ClusterManagers.jl**](https://github.com/JuliaParallel/ClusterManagers.jl) package is very versatile.
+
+## SLURM batch script
+
+To submit a batch script to [SLURM](https://slurm.schedmd.com/documentation.html), we start by creating a file named `run.batch` with the following content:
+
+```bash
+#!/bin/bash
+#SBATCH --job-name=example
+#SBATCH --output=output.out
+#SBATCH --account=your_account
+#SBATCH --nodes=10
+#SBATCH --ntasks-per-node=1
+#SBATCH --cpus-per-task=72
+#SBATCH --mem=128GB
+#SBATCH --time=0:10:00
+#SBATCH --qos=parallel
+
+# Set PATH to include the directory of your custom Julia installation
+export PATH=/home/username/.juliaup/bin:$PATH
+
+# Now run Julia
+julia --project script.jl
+```
+
+where we have to replace `your_account` with the name of your account. This script will be used to submit the job to the cluster by using the following command in terminal:
+
+```shell
+sbatch run.batch
+```
+
+Here, we are requesting `10` nodes with `72` threads each (`720` parallel jobs). The `--time` flag specifies the maximum time that the job can run. To see all the available options, you can check the [SLURM documentation](https://slurm.schedmd.com/documentation.html). We also export the path to the custom Julia installation, which is necessary to run the script (replace `username` with your username). Finally, we run the script `script.jl` with the command `julia --project script.jl`.
+
+In the following, we will consider two examples:
+
+1. **Parallelization of a Monte Carlo quantum trajectories**
+2. **Parallelization of a Master Equation by sweeping over parameters**
+
+## Monte Carlo Quantum Trajectories
+
+Let's consider a `2`-dimensional transverse field Ising model with `4x3` spins. The Hamiltonian is given by
+
+```math
+\hat{H} = \frac{J_z}{2} \sum_{\langle i,j \rangle} \hat{\sigma}_i^z \hat{\sigma}_j^z + h_x \sum_i \hat{\sigma}_i^x \, ,
+```
+
+where the sums are over nearest neighbors, and the collapse operators are given by
+
+```math
+\hat{c}_i = \sqrt{\gamma} \hat{\sigma}_i^- \, .
+```
+
+In this case, the `script.jl` contains the following content:
+
+```julia
+using Distributed
+using ClusterManagers
+
+const SLURM_NUM_TASKS = parse(Int, ENV["SLURM_NTASKS"])
+const SLURM_CPUS_PER_TASK = parse(Int, ENV["SLURM_CPUS_PER_TASK"])
+
+exeflags = ["--project=.", "-t $SLURM_CPUS_PER_TASK"]
+addprocs(SlurmManager(SLURM_NUM_TASKS); exeflags=exeflags, topology=:master_worker)
+
+
+println("################")
+println("Hello! You have $(nworkers()) workers with $(remotecall_fetch(Threads.nthreads, 2)) threads each.")
+
+println("----------------")
+
+
+println("################")
+
+flush(stdout)
+
+@everywhere begin
+ using QuantumToolbox
+ using OrdinaryDiffEq
+
+ BLAS.set_num_threads(1)
+end
+
+# Define lattice
+
+Nx = 4
+Ny = 3
+latt = Lattice(Nx = Nx, Ny = Ny)
+
+# Define Hamiltonian and collapse operators
+Jx = 0.0
+Jy = 0.0
+Jz = 1.0
+hx = 0.2
+hy = 0.0
+hz = 0.0
+γ = 1
+
+Sx = mapreduce(i -> multisite_operator(latt, i=>sigmax()), +, 1:latt.N)
+Sy = mapreduce(i -> multisite_operator(latt, i=>sigmay()), +, 1:latt.N)
+Sz = mapreduce(i -> multisite_operator(latt, i=>sigmaz()), +, 1:latt.N)
+
+H, c_ops = DissipativeIsing(Jx, Jy, Jz, hx, hy, hz, γ, latt; boundary_condition = Val(:periodic_bc), order = 1)
+e_ops = [Sx, Sy, Sz]
+
+# Time Evolution
+
+ψ0 = fock(2^latt.N, 0, dims = ntuple(i->2, Val(latt.N)))
+
+tlist = range(0, 10, 100)
+
+sol_mc = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ntraj=5000, ensemblealg=EnsembleSplitThreads())
+
+##
+
+println("FINISH!")
+
+rmprocs(workers())
+```
+
+In this script, we first load the necessary packages for distributed computing on the cluster (`Distributed.jl` and `ClusterManagers.jl`). Thanks to the environment variables (previously defined in the SLURM script), we can define the number of tasks and the number of CPUs per task. Then, we initialize the distributed network with the `addprocs(SlurmManager(SLURM_NUM_TASKS); exeflags=exeflags, topology=:master_worker)` command. We then import the packages with the `@everywhere` macro, meaning to load them in all the workers. Moreover, in order to avoid conflicts between the multithreading of the BLAS library and the native Julia multithreading, we set the number of threads of the BLAS library to 1 with the `BLAS.set_num_threads(1)` command. More information about this can be found [here](https://docs.julialang.org/en/v1/manual/performance-tips/#man-multithreading-linear-algebra).
+
+With the
+
+```julia
+println("Hello! You have $(nworkers()) workers with $(remotecall_fetch(Threads.nthreads, 2)) threads each.")
+```
+
+command, we test that the distributed network is correctly initialized. The `remotecall_fetch(Threads.nthreads, 2)` command returns the number of threads of the worker with ID `2`.
+
+We then write the main part of the script, where we define the lattice through the [`Lattice`](@ref) function. We set the parameters and define the Hamiltonian and collapse operators with the [`DissipativeIsing`](@ref) function. We also define the expectation operators `e_ops` and the initial state `ψ0`. Finally, we perform the Monte Carlo quantum trajectories with the [`mcsolve`](@ref) function. The `ensemblealg=EnsembleSplitThreads()` argument is used to parallelize the Monte Carlo quantum trajectories, by splitting the ensemble of trajectories among the workers. For a more detailed explanation of the different ensemble methods, you can check the [official documentation](https://docs.sciml.ai/DiffEqDocs/stable/features/ensemble/) of the [**DifferentialEquations.jl**](https://github.com/SciML/DifferentialEquations.jl/) package. Finally, the `rmprocs(workers())` command is used to remove the workers after the computation is finished.
+
+The output of the script will be printed in the `output.out` file, which contains an output similar to the following:
+
+```
+################
+Hello! You have 10 workers with 72 threads each.
+----------------
+################
+
+Progress: [==============================] 100.0% --- Elapsed Time: 0h 00m 21s (ETA: 0h 00m 00s)
+
+FINISH!
+```
+
+where we can see that the computation **lasted only 21 seconds**.
+
+## Master Equation by Sweeping Over Parameters
+
+In this example, we will consider a driven Jaynes-Cummings model, describing a two-level atom interacting with a driven cavity mode. The Hamiltonian is given by
+
+```math
+\hat{H} = \omega_c \hat{a}^\dagger \hat{a} + \frac{\omega_q}{2} \hat{\sigma}_z + g (\hat{a} \hat{\sigma}_+ + \hat{a}^\dagger \hat{\sigma}_-) + F \cos(\omega_d t) (\hat{a} + \hat{a}^\dagger) \, ,
+```
+
+and the collapse operators are given by
+
+```math
+\hat{c}_1 = \sqrt{\gamma} \hat{a} \, , \quad \hat{c}_2 = \sqrt{\gamma} \hat{\sigma}_- \, .
+```
+
+The SLURM batch script file is the same as before, but the `script.jl` file now contains the following content:
+
+```julia
+using Distributed
+using ClusterManagers
+
+const SLURM_NUM_TASKS = parse(Int, ENV["SLURM_NTASKS"])
+const SLURM_CPUS_PER_TASK = parse(Int, ENV["SLURM_CPUS_PER_TASK"])
+
+exeflags = ["--project=.", "-t $SLURM_CPUS_PER_TASK"]
+addprocs(SlurmManager(SLURM_NUM_TASKS); exeflags=exeflags, topology=:master_worker)
+
+
+println("################")
+println("Hello! You have $(nworkers()) workers with $(remotecall_fetch(Threads.nthreads, 2)) threads each.")
+
+println("----------------")
+
+
+println("################")
+
+flush(stdout)
+
+@everywhere begin
+ using QuantumToolbox
+ using OrdinaryDiffEq
+
+ BLAS.set_num_threads(1)
+end
+
+@everywhere begin
+ const Nc = 20
+ const ωc = 1.0
+ const g = 0.05
+ const γ = 0.01
+ const F = 0.01
+
+ const a = tensor(destroy(Nc), qeye(2))
+
+ const σm = tensor(qeye(Nc), sigmam())
+ const σp = tensor(qeye(Nc), sigmap())
+
+ H(ωq) = ωc * a' * a + ωq * tensor(num(Nc), qeye(2)) + g * (a' * σm + a * σp)
+
+ coef(p, t) = p.F * cos(p.ωd * t) # coefficient for the time-dependent term
+
+ const c_ops = [sqrt(γ) * a, sqrt(γ) * σm]
+ const e_ops = [a' * a]
+end
+
+# Define the ODE problem and the EnsembleProblem generation function
+
+@everywhere begin
+ ωq_list = range(ωc - 3*g, ωc + 3*g, 100)
+ ωd_list = range(ωc - 3*g, ωc + 3*g, 100)
+
+ const iter = collect(Iterators.product(ωq_list, ωd_list))
+
+ function my_prob_func(prob, i, repeat, channel)
+ ωq, ωd = iter[i]
+ H_i = H(ωq)
+ H_d_i = H_i + QobjEvo(a + a', coef) # Hamiltonian with a driving term
+
+ L = liouvillian(H_d_i, c_ops).data # Make the Liouvillian
+
+ put!(channel, true) # Update the progress bar channel
+
+ remake(prob, f=L, p=(F = F, ωd = ωd))
+ end
+end
+
+ωq, ωd = iter[1]
+H0 = H(ωq) + QobjEvo(a + a', coef)
+ψ0 = tensor(fock(Nc, 0), basis(2, 1)) # Ground State
+tlist = range(0, 20 / γ, 1000)
+
+prob = mesolveProblem(H0, ψ0, tlist, c_ops, e_ops=e_ops, progress_bar=Val(false), params=(F = F, ωd = ωd))
+
+### Just to print the progress bar
+progr = ProgressBar(length(iter))
+progr_channel::RemoteChannel{Channel{Bool}} = RemoteChannel(() -> Channel{Bool}(1))
+###
+ens_prob = EnsembleProblem(prob.prob, prob_func=(prob, i, repeat) -> my_prob_func(prob, i, repeat, progr_channel))
+
+
+@sync begin
+ @async while take!(progr_channel)
+ next!(progr)
+ end
+
+ @async begin
+ sol = solve(ens_prob, Tsit5(), EnsembleSplitThreads(), trajectories = length(iter))
+ put!(progr_channel, false)
+ end
+end
+```
+
+We are using the [`mesolveProblem`](@ref) function to define the master equation problem. We added some code to manage the progress bar, which is updated through a `RemoteChannel`. The `prob_func` argument of the `EnsembleProblem` function is used to define the function that generates the problem for each iteration. The `iter` variable contains the product of the `ωq_list` and `ωd_list` lists, which are used to sweep over the parameters. The `sol = solve(ens_prob, Tsit5(), EnsembleDistributed(), trajectories=length(iter))` command is used to solve the problem with the distributed ensemble method. The output of the script will be printed in the `output.out` file, which contains an output similar to the previous example.
+
+
diff --git a/docs/src/users_guide/extensions/cairomakie.md b/docs/src/users_guide/extensions/cairomakie.md
new file mode 100644
index 000000000..aa4512f06
--- /dev/null
+++ b/docs/src/users_guide/extensions/cairomakie.md
@@ -0,0 +1,23 @@
+# [Extension for the Makie.jl ecosystem](@id doc:Makie)
+
+This is an extension to support visualization (plotting functions) using [`Makie.jl`](https://github.com/MakieOrg/Makie.jl) library.
+
+This extension will be automatically loaded if user imports both `QuantumToolbox.jl` and [`Makie.jl`](https://github.com/MakieOrg/Makie.jl). It is worth noting that the `Makie.jl` package provides only the engine for plotting, and the user has to import the specific backend. Here we demonstrate the usage of [`CairoMakie.jl`](https://github.com/MakieOrg/Makie.jl/tree/master/CairoMakie), which will automatically import `Makie.jl`.
+
+```julia
+using QuantumToolbox
+using CairoMakie
+```
+
+To plot with [`CairoMakie.jl`](https://github.com/MakieOrg/Makie.jl/tree/master/CairoMakie) library, specify the keyword argument `library = Val(:Makie)` for the plotting functions.
+
+!!! warning "Beware of type-stability!"
+ If you want to keep type stability, it is recommended to use `Val(:Makie)` instead of `:Makie`. 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.
+
+The supported plotting functions are listed as follows:
+
+| **Plotting Function** | **Description** |
+|:----------------------|:----------------|
+| [`plot_wigner`](@ref) | [Wigner quasipropability distribution](https://en.wikipedia.org/wiki/Wigner_quasiprobability_distribution) |
+| [`plot_fock_distribution`](@ref) | [Fock state](https://en.wikipedia.org/wiki/Fock_state) distribution |
+| [`plot_bloch`](@ref) | [Plotting on the Bloch Sphere](@ref doc:Plotting-on-the-Bloch-Sphere) |
diff --git a/docs/src/users_guide/extensions/cuda.md b/docs/src/users_guide/extensions/cuda.md
index e40c90915..eacef8083 100644
--- a/docs/src/users_guide/extensions/cuda.md
+++ b/docs/src/users_guide/extensions/cuda.md
@@ -4,10 +4,7 @@
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.
-!!! note "Requirements"
- The [`CUDA.jl`](https://github.com/JuliaGPU/CUDA.jl) extension for `QuantumToolbox.jl` requires `Julia 1.9+`.
-
-This extension will be automatically loaded if user imports both `QuantumToolbox` and [`CUDA.jl`](https://github.com/JuliaGPU/CUDA.jl):
+This extension will be automatically loaded if user imports both `QuantumToolbox.jl` and [`CUDA.jl`](https://github.com/JuliaGPU/CUDA.jl):
```julia
using QuantumToolbox
@@ -39,7 +36,7 @@ V = fock(2, 0) # CPU dense vector
```
```
-Quantum Object: type=Ket dims=[2] size=(2,)
+Quantum Object: type=Ket() dims=[2] size=(2,)
2-element Vector{ComplexF64}:
1.0 + 0.0im
0.0 + 0.0im
@@ -50,7 +47,7 @@ cu(V)
```
```
-Quantum Object: type=Ket dims=[2] size=(2,)
+Quantum Object: type=Ket() dims=[2] size=(2,)
2-element CuArray{ComplexF64, 1, CUDA.DeviceMemory}:
1.0 + 0.0im
0.0 + 0.0im
@@ -61,7 +58,7 @@ cu(V; word_size = 32)
```
```
-Quantum Object: type=Ket dims=[2] size=(2,)
+Quantum Object: type=Ket() dims=[2] size=(2,)
2-element CuArray{ComplexF32, 1, CUDA.DeviceMemory}:
1.0 + 0.0im
0.0 + 0.0im
@@ -72,7 +69,7 @@ M = Qobj([1 2; 3 4]) # CPU dense matrix
```
```
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=false
+Quantum Object: type=Operator() dims=[2] size=(2, 2) ishermitian=false
2×2 Matrix{Int64}:
1 2
3 4
@@ -83,7 +80,7 @@ cu(M)
```
```
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=false
+Quantum Object: type=Operator() dims=[2] size=(2, 2) ishermitian=false
2×2 CuArray{Int64, 2, CUDA.DeviceMemory}:
1 2
3 4
@@ -94,7 +91,7 @@ cu(M; word_size = 32)
```
```
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=false
+Quantum Object: type=Operator() dims=[2] size=(2, 2) ishermitian=false
2×2 CuArray{Int32, 2, CUDA.DeviceMemory}:
1 2
3 4
@@ -107,7 +104,7 @@ V = fock(2, 0; sparse=true) # CPU sparse vector
```
```
-Quantum Object: type=Ket dims=[2] size=(2,)
+Quantum Object: type=Ket() dims=[2] size=(2,)
2-element SparseVector{ComplexF64, Int64} with 1 stored entry:
[1] = 1.0+0.0im
```
@@ -117,7 +114,7 @@ cu(V)
```
```
-Quantum Object: type=Ket dims=[2] size=(2,)
+Quantum Object: type=Ket() dims=[2] size=(2,)
2-element CuSparseVector{ComplexF64, Int32} with 1 stored entry:
[1] = 1.0+0.0im
```
@@ -127,7 +124,7 @@ cu(V; word_size = 32)
```
```
-Quantum Object: type=Ket dims=[2] size=(2,)
+Quantum Object: type=Ket() dims=[2] size=(2,)
2-element CuSparseVector{ComplexF32, Int32} with 1 stored entry:
[1] = 1.0+0.0im
```
@@ -137,7 +134,7 @@ M = sigmax() # CPU sparse matrix
```
```
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
+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 ⋅
@@ -148,7 +145,7 @@ cu(M)
```
```
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
+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 ⋅
@@ -159,7 +156,7 @@ cu(M; word_size = 32)
```
```
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
+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/plotting_the_bloch_sphere.md b/docs/src/users_guide/plotting_the_bloch_sphere.md
new file mode 100644
index 000000000..a306d2977
--- /dev/null
+++ b/docs/src/users_guide/plotting_the_bloch_sphere.md
@@ -0,0 +1,276 @@
+# [Plotting on the Bloch Sphere](@id doc:Plotting-on-the-Bloch-Sphere)
+
+```@setup Bloch_sphere_rendering
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+## Introduction
+
+When studying the dynamics of a two-level system, it's often convenient to visualize the state of the system by plotting the state vector or density matrix on the Bloch sphere.
+
+In [`QuantumToolbox`](https://qutip.org/QuantumToolbox.jl/), this can be done using the [`Bloch`](@ref) or [`plot_bloch`](@ref) methods that provide same syntax as [QuTiP](https://qutip.readthedocs.io/en/stable/guide/guide-bloch.html).
+
+## Create a Bloch Sphere
+
+In [`QuantumToolbox`](https://qutip.org/QuantumToolbox.jl/), creating a [`Bloch`](@ref) sphere is accomplished by calling either:
+
+!!! note "Import plotting libraries"
+ Remember to import plotting libraries first. Here, we demonstrate the functionalities with [`CairoMakie.jl`](https://docs.makie.org/stable/explanations/backends/cairomakie.html).
+
+```@example Bloch_sphere_rendering
+b = Bloch()
+```
+
+which will load an instance of [`Bloch`](@ref). Before getting into the details of these objects, we can simply plot the blank [`Bloch`](@ref) sphere associated with these instances via:
+
+```@example Bloch_sphere_rendering
+fig, _ = render(b)
+fig
+```
+
+See the [API documentation for Bloch sphere](@ref doc-API:Bloch-Sphere) for a full list of other available functions.
+
+## Add a single data point
+
+As an example, we can add a single data point via [`add_points!`](@ref):
+
+```@example Bloch_sphere_rendering
+pnt = [1 / sqrt(3), 1 / sqrt(3), 1 / sqrt(3)]
+add_points!(b, pnt)
+fig, _ = render(b)
+fig
+```
+
+## Add a single vector
+
+Add a single vector via [`add_vectors!`](@ref):
+
+```@example Bloch_sphere_rendering
+vec = [0, 1, 0]
+add_vectors!(b, vec)
+fig, _ = render(b)
+fig
+```
+
+## Add a single quantum state
+
+Add another vector corresponding to the ``|0\rangle`` state:
+
+```@example Bloch_sphere_rendering
+z0 = basis(2, 0)
+add_states!(b, z0)
+fig, _ = render(b)
+fig
+```
+
+## Add multiple data
+
+We can also plot multiple points, vectors, and states at the same time by passing arrays instead of individual elements via [`add_points!`](@ref), [`add_vectors!`](@ref), and [`add_states!`](@ref), respectively. Before giving an example, we can use [`clear!`](@ref) to remove the current data from our [`Bloch`](@ref) sphere instead of creating a new instance:
+
+```@example Bloch_sphere_rendering
+clear!(b)
+fig, _ = render(b)
+fig
+```
+
+Now on the same [`Bloch`](@ref) sphere, we can plot the three states via [`add_states!`](@ref) associated with the `x`, `y`, and `z` directions:
+
+```@example Bloch_sphere_rendering
+x = basis(2, 0) + basis(2, 1)
+y = basis(2, 0) + im * basis(2, 1)
+z = basis(2, 0)
+add_states!(b, [x, y, z])
+fig, _ = render(b)
+fig
+```
+
+!!! note "State normalization"
+ The function [`add_states!`](@ref) will automatically normalize the given quantum state(s), while [`add_vectors!`](@ref) does not normalize the given vectors.
+
+A similar method works for adding vectors:
+
+```@example Bloch_sphere_rendering
+clear!(b)
+vecs = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
+add_vectors!(b, vecs)
+fig, _ = render(b)
+fig
+```
+
+# Add lines and arcs
+
+You can also add lines and arcs via [`add_line!`](@ref) and [`add_arc!`](@ref) respectively:
+
+```@example Bloch_sphere_rendering
+add_line!(b, x, y)
+add_arc!(b, y, z)
+fig, _ = render(b)
+fig
+```
+
+## Add multiple points
+
+Adding multiple points to the [`Bloch`](@ref) sphere works slightly differently than adding multiple states or vectors. For example, lets add a set of `20` points around the equator (after calling [`clear!`](@ref)):
+
+```@example Bloch_sphere_rendering
+clear!(b)
+
+th = LinRange(0, 2π, 20)
+xp = cos.(th)
+yp = sin.(th)
+zp = zeros(20)
+pnts = [xp, yp, zp]
+add_points!(b, pnts)
+fig, lscene = render(b)
+fig
+```
+
+Notice that, in contrast to states or vectors, each point remains the same color as the initial point. This is because adding multiple data points using [`add_points!`](@ref) is interpreted, by default, to correspond to a single data point (single qubit state) plotted at different times. This is very useful when visualizing the dynamics of a qubit. If we want to plot additional qubit states we can call additional [`add_points!`](@ref) function:
+
+```@example Bloch_sphere_rendering
+xz = zeros(20)
+yz = sin.(th)
+zz = cos.(th)
+add_points!(b, [xz, yz, zz])
+fig, lscene = render(b)
+fig
+```
+
+The color and shape of the data points is varied automatically by [`Bloch`](@ref). Notice how the color and point markers change for each set of data. Again, we have had to call [`add_points!`](@ref) twice because adding more than one set of multiple data points is not supported by the [`add_points!`](@ref) function.
+
+What if we want to vary the color of our points. We can tell [`Bloch`](@ref) to vary the color of each point according to the colors listed in the `point_color` field (see [Configuring the Bloch sphere](@ref doc:Configuring-the-Bloch-sphere) below). Again, after [`clear!`](@ref):
+
+```@example Bloch_sphere_rendering
+clear!(b)
+
+xp = cos.(th)
+yp = sin.(th)
+zp = zeros(20)
+pnts = [xp, yp, zp]
+add_points!(b, pnts, meth=:m) # add `meth=:m` to signify 'multi' colored points
+fig, lscene = render(b)
+fig
+```
+
+Now, the data points cycle through a variety of predefined colors. Now lets add another set of points, but this time we want the set to be a single color, representing say a qubit going from the ``|0\rangle`` state to the ``|1\rangle`` state in the `y-z` plane:
+
+```@example Bloch_sphere_rendering
+pnts = [xz, yz, zz]
+add_points!(b, pnts) # no `meth=:m`
+fig, lscene = render(b)
+fig
+```
+
+## [Configuring the Bloch sphere](@id doc:Configuring-the-Bloch-sphere)
+
+At the end of the last section we saw that the colors and marker shapes of the data plotted on the Bloch sphere are automatically varied according to the number of points and vectors added. But what if you want a different choice of color, or you want your sphere to be purple with different axes labels? Well then you are in luck as the [`Bloch`](@ref) structure has many fields which one can control. Assuming `b = Bloch()`:
+
+### Data storage
+
+| **Field** | **Description** | **Default setting** |
+|:----------|:----------------|:--------------------|
+| `b.points` | Points to plot on the Bloch sphere (3D coordinates) | `Vector{Matrix{Float64}}()` (empty) |
+| `b.vectors` | Vectors to plot on the Bloch sphere | `Vector{Vector{Float64}}()` (empty) |
+| `b.lines` | Lines to draw on the sphere with each line given as `([start_pt, end_pt], line_format)` | `Vector{Tuple{Vector{Vector{Float64}},String}}()` (empty) |
+| `b.arcs` | Arcs to draw on the sphere | `Vector{Vector{Vector{Float64}}}()` (empty) |
+
+### Properties
+
+| **Field** | **Description** | **Default setting** |
+|:----------|:----------------|:--------------------|
+| `b.font_color` | Color of axis labels and text | `"black"` |
+| `b.font_size` | Font size for labels | `20` |
+| `b.frame_alpha` | Transparency of the wire frame | `0.2` |
+| `b.frame_color` | Color of the wire frame | `"gray"` |
+| `b.frame_width` | Width of wire frame | `1.0` |
+| `b.point_default_color` | Default color cycle for points | `["blue", "red", "green", "#CC6600"]` |
+| `b.point_color` | List of colors for Bloch point markers to cycle through | `Union{Nothing,String}[]` |
+| `b.point_marker` | List of point marker shapes to cycle through | `[:circle, :rect, :diamond, :utriangle]` |
+| `b.point_size` | List of point marker sizes (not all markers look the same size when plotted) | `[5.5, 6.2, 6.5, 7.5]` |
+| `b.point_style` | List of marker styles | `Symbol[]` |
+| `b.point_alpha` | List of marker transparencies | `Float64[]` |
+| `b.sphere_color` | Color of Bloch sphere surface | `0.2` |
+| `b.sphere_alpha` | Transparency of sphere surface | `"#FFDDDD"` |
+| `b.vector_color` | Colors for vectors | `["green", "#CC6600", "blue", "red"]` |
+| `b.vector_width` | Width of vectors | `0.02` |
+| `b.vector_tiplength` | Length of vector arrow head | `0.08` |
+| `b.vector_tipradius` | Radius of vector arrow head | `0.05` |
+| `b.view` | Azimuthal and elevation viewing angles in degrees | `[30, 30]` |
+| `b.xlabel` | Labels for x-axis | `[L"x", ""]` (``+x`` and ``-x``) |
+| `b.xlpos` | Positions of x-axis labels | `[1.2, -1.2]` |
+| `b.ylabel` | Labels for y-axis | `[L"y", ""]` (``+y`` and ``-y``) |
+| `b.ylpos` | Positions of y-axis labels | `[1.2, -1.2]` |
+| `b.zlabel` | Labels for z-axis | `[L"\|0\rangle", L"\|1\rangle]"` (``+z`` and ``-z``) |
+| `b.zlpos` | Positions of z-axis labels | `[1.2, -1.2]` |
+
+These properties can also be accessed via the `print` command:
+
+```@example Bloch_sphere_rendering
+b = Bloch()
+print(b)
+```
+
+## Animating with the Bloch sphere
+
+The [`Bloch`](@ref) structure was designed from the outset to generate animations. To animate a set of vectors or data points, the basic idea is: plot the data at time ``t_1``, save the sphere, clear the sphere, plot data at ``t_2``, and so on. The easiest way to animate data on the Bloch sphere is to use the `record` function provided by [`Makie.jl`](https://docs.makie.org/stable/). We will demonstrate this functionality with the following example: the decay of a qubit on the Bloch sphere.
+
+```@example Bloch_sphere_rendering
+# system parameters
+ω = 2π
+θ = 0.2π
+n_th = 0.5 # temperature
+γ1 = 0.5
+γ2 = 0.2
+
+# operators and the Hamiltonian
+sx = sigmax()
+sy = sigmay()
+sz = sigmaz()
+sm = sigmam()
+H = ω * (cos(θ) * sz + sin(θ) * sx)
+
+# collapse operators
+c_op_list = (
+ √(γ1 * (n_th + 1)) * sm,
+ √(γ1 * n_th) * sm',
+ √γ2 * sz
+)
+
+# solving evolution
+ψ0 = basis(2, 0)
+tlist = LinRange(0, 4, 250)
+sol = mesolve(H, ψ0, tlist, c_op_list, e_ops = (sx, sy, sz), progress_bar = Val(false))
+```
+
+To animate a set of vectors or data points, we use the `record` function provided by [`Makie.jl`](https://docs.makie.org/stable/):
+
+```@example Bloch_sphere_rendering
+# expectation values
+x = real(sol.expect[1,:])
+y = real(sol.expect[2,:])
+z = real(sol.expect[3,:])
+
+# create Bloch sphere
+b = Bloch()
+b.view = [50,30]
+fig, lscene = render(b)
+
+# save animation
+record(fig, "qubit_decay.mp4", eachindex(tlist), framerate = 20) do idx
+ clear!(b)
+ add_vectors!(b, [sin(θ), 0, cos(θ)])
+ add_points!(b, [x[1:idx], y[1:idx], z[1:idx]])
+ render(b, location = lscene)
+end
+nothing # hide
+```
+
+```@raw html
+
+```
+
+!!! note
+ Here, we set the keyword argument `location = lscene` in the last `render` function to update the existing Bloch sphere without creating new `Figure` and `LScene`. This is efficient when drawing animations.
diff --git a/docs/src/users_guide/settings.md b/docs/src/users_guide/settings.md
new file mode 100644
index 000000000..c2c530223
--- /dev/null
+++ b/docs/src/users_guide/settings.md
@@ -0,0 +1,37 @@
+# [QuantumToolbox Settings](@id doc:QuantumToolbox-Settings)
+
+In this section, we introduce the default global settings used throughout the package and show how to modify them.
+
+All settings are stored in [`QuantumToolbox.settings`](@ref).
+
+!!! warning "Differences from QuTiP"
+ Due to the differences in programming languages, solving algorithms, and many other reasons, these global settings (including their default values and usage) may be very different from those in `Python QuTiP`.
+
+## List of settings
+
+Here, we list out each setting along with the specific functions that will use it.
+
+- `tidyup_tol::Float64 = 1e-14` : tolerance for [`tidyup`](@ref) and [`tidyup!`](@ref).
+- `auto_tidyup::Bool = true` : Automatically tidyup during the following situations:
+ * Solving for eigenstates, including [`eigenstates`](@ref), [`eigsolve`](@ref), [`eigsolve_al`](@ref)
+ * Creating [`bloch_redfield_tensor`](@ref) or [`brterm`](@ref), and solving [`brmesolve`](@ref).
+- (to be announced)
+
+## Change default settings
+
+First, we can check the current [`QuantumToolbox.settings`](@ref):
+
+```@example settings
+using QuantumToolbox
+
+QuantumToolbox.settings
+```
+
+Next, one can overwrite the default settings by
+
+```@example settings
+QuantumToolbox.settings.tidyup_tol = 1e-10
+QuantumToolbox.settings.auto_tidyup = false
+
+QuantumToolbox.settings
+```
\ No newline at end of file
diff --git a/docs/src/users_guide/states_and_operators.md b/docs/src/users_guide/states_and_operators.md
index 565aece24..4795756c3 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 operator 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. For example, [`fidelity`](@ref), and trace distance ([`tracedist`](@ref)) are included. For more metric functions, see section [Entropy and Metrics](@ref doc-API:Entropy-and-Metrics) in the API page.
+
+```@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-Chuang2011](@citet). It is the square root of the fidelity defined in [Jozsa1994](@citet). 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) (or [`operator_to_vector`](@ref)) and [`vec2mat`](@ref) (or [`vector_to_operator`](@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{spost}(\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 [`QuantumObject`](@ref) of type [`SuperOperator`](@ref) 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 [Lindblad Master Equation Solver](@ref doc-TE:Lindblad-Master-Equation-Solver) for more details.
diff --git a/docs/src/users_guide/steadystate.md b/docs/src/users_guide/steadystate.md
new file mode 100644
index 000000000..a74353b90
--- /dev/null
+++ b/docs/src/users_guide/steadystate.md
@@ -0,0 +1,120 @@
+# [Solving for Steady-State Solutions](@id doc:Solving-for-Steady-State-Solutions)
+
+## Introduction
+
+For time-independent open quantum systems with decay rates larger than the corresponding excitation rates, the system will tend toward a steady state as ``t\rightarrow\infty`` that satisfies the equation
+
+```math
+\frac{d\hat{\rho}_{\textrm{ss}}}{dt} = \mathcal{L}\hat{\rho}_{\textrm{ss}}=0.
+```
+
+Although the requirement for time-independence seems quite restrictive, one can often employ a transformation to the interaction picture that yields a time-independent Hamiltonian. For many these systems, solving for the asymptotic density matrix ``\hat{\rho}_{\textrm{ss}}`` can be achieved using direct or iterative solution methods faster than using master equation or Monte-Carlo simulations. Although the steady state equation has a simple mathematical form, the properties of the Liouvillian operator are such that the solutions to this equation are anything but straightforward to find.
+
+## Steady State solvers in `QuantumToolbox.jl`
+In `QuantumToolbox.jl`, the steady-state solution for a system Hamiltonian or Liouvillian is given by [`steadystate`](@ref). This function implements a number of different methods for finding the steady state, each with their own pros and cons, where the method used can be chosen using the `solver` keyword argument.
+
+| **Solver** | **Description** |
+|:-----------|:----------------|
+| [`SteadyStateDirectSolver()`](@ref SteadyStateDirectSolver) | Directly solve ``Ax=b`` using the standard method given in `Julia` `LinearAlgebra` |
+| [`SteadyStateLinearSolver()`](@ref SteadyStateLinearSolver) | Directly solve ``Ax=b`` using the algorithms given in [`LinearSolve.jl`](https://docs.sciml.ai/LinearSolve/stable/) |
+| [`SteadyStateEigenSolver()`](@ref SteadyStateEigenSolver) | Find the zero (or lowest) eigenvalue of ``\mathcal{L}`` using [`eigsolve`](@ref) |
+| [`SteadyStateODESolver()`](@ref SteadyStateODESolver) | Solving time evolution with algorithms given in [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) |
+
+## Using Steady State solvers
+
+The function [`steadystate`](@ref) can take either a Hamiltonian and a list of collapse operators `c_ops` as input, generating internally the corresponding Liouvillian ``\mathcal{L}`` in Lindblad form, or alternatively, a Liouvillian ``\mathcal{L}`` passed by the user.
+
+```julia
+ρ_ss = steadystate(H) # Hamiltonian
+ρ_ss = steadystate(H, c_ops) # Hamiltonian and collapse operators
+ρ_ss = steadystate(L) # Liouvillian
+```
+where `H` is a quantum object representing the system Hamiltonian ([`Operator`](@ref)) or Liouvillian ([`SuperOperator`](@ref)), and `c_ops` (defaults to `nothing`) can be a list of [`QuantumObject`](@ref) for the system collapse operators. The output, labelled as `ρ_ss`, is the steady-state solution for the systems. If no other keywords are passed to the function, the default solver [`SteadyStateDirectSolver()`](@ref SteadyStateDirectSolver) is used.
+
+To change a solver, use the keyword argument `solver`, for example:
+
+```julia
+ρ_ss = steadystate(H, c_ops; solver = SteadyStateLinearSolver())
+```
+
+Although it is not obvious, the [`SteadyStateDirectSolver()`](@ref SteadyStateDirectSolver) and [`SteadyStateEigenSolver()`](@ref SteadyStateEigenSolver) methods all use an LU decomposition internally and thus can have a large memory overhead. In contrast, for [`SteadyStateLinearSolver()`](@ref SteadyStateLinearSolver), iterative algorithms provided by [`LinearSolve.jl`](https://docs.sciml.ai/LinearSolve/stable/solvers/solvers/), such as `KrylovJL_GMRES()` and `KrylovJL_BICGSTAB()`, do not factor the matrix and thus take less memory than the LU methods and allow, in principle, for extremely large system sizes. The downside is that these methods can take much longer than the direct method as the condition number of the Liouvillian matrix is large, indicating that these iterative methods require a large number of iterations for convergence.
+
+To overcome this, one can provide preconditioner that solves for an approximate inverse for the (modified) Liouvillian, thus better conditioning the problem, leading to faster convergence. The left and right preconditioner can be specified as the keyword argument `Pl` and `Pr`, respectively:
+```julia
+solver = SteadyStateLinearSolver(alg = KrylovJL_GMRES(), Pl = Pl, Pr = Pr)
+```
+The use of a preconditioner can actually make these iterative methods faster than the other solution methods. The problem with precondioning is that it is only well defined for Hermitian matrices. Since the Liouvillian is non-Hermitian, the ability to find a good preconditioner is not guaranteed. Moreover, if a preconditioner is found, it is not guaranteed to have a good condition number.
+
+Furthermore, `QuantiumToolbox` can take advantage of the Intel (Pardiso) LU solver in the Intel Math Kernel library (Intel-MKL) by using [`LinearSolve.jl`](https://docs.sciml.ai/LinearSolve/stable/) and either [`Pardiso.jl`](https://github.com/JuliaSparse/Pardiso.jl) or [`MKL_jll.jl`](https://github.com/JuliaBinaryWrappers/MKL_jll.jl):
+
+```julia
+using QuantumToolbox
+using LinearSolve # must be loaded
+
+using Pardiso
+solver = SteadyStateLinearSolver(alg = MKLPardisoFactorize())
+
+using MKL_jll
+solver = SteadyStateLinearSolver(alg = MKLLUFactorization())
+```
+
+See [`LinearSolve.jl` Solvers](https://docs.sciml.ai/LinearSolve/stable/solvers/solvers/) for more details.
+
+## Example: Harmonic oscillator in thermal bath
+
+Here, we demonstrate [`steadystate`](@ref) by using the example with the harmonic oscillator in thermal bath from the previous section ([Lindblad Master Equation Solver](@ref doc-TE:Lindblad-Master-Equation-Solver)).
+
+```@example steady_state_example
+using QuantumToolbox
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+
+# Define parameters
+N = 20 # number of basis states to consider
+a = destroy(N)
+H = a' * a
+ψ0 = basis(N, 10) # initial state
+κ = 0.1 # coupling to oscillator
+n_th = 2 # temperature with average of 2 excitations
+
+# collapse operators
+c_ops = [
+ sqrt(κ * (n_th + 1)) * a, # emission
+ sqrt(κ * n_th ) * a' # absorption
+]
+
+# find steady-state solution
+ρ_ss = steadystate(H, c_ops)
+
+# find expectation value for particle number in steady state
+e_ops = [a' * a]
+exp_ss = real(expect(e_ops[1], ρ_ss))
+
+tlist = LinRange(0, 50, 100)
+
+# monte-carlo
+sol_mc = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ntraj=100, progress_bar=false)
+exp_mc = real(sol_mc.expect[1, :])
+
+# master eq.
+sol_me = mesolve(H, ψ0, tlist, c_ops, e_ops=e_ops, progress_bar=false)
+exp_me = real(sol_me.expect[1, :])
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1],
+ title = L"Decay of Fock state $|10\rangle$ in a thermal environment with $\langle n\rangle=2$",
+ xlabel = "Time",
+ ylabel = "Number of excitations",
+)
+lines!(ax, tlist, exp_mc, label = "Monte-Carlo", linewidth = 2, color = :blue)
+lines!(ax, tlist, exp_me, label = "Master Equation", linewidth = 2, color = :orange, linestyle = :dash)
+lines!(ax, tlist, exp_ss .* ones(length(tlist)), label = "Steady State", linewidth = 2, color = :red)
+axislegend(ax, position = :rt)
+
+fig
+```
+
+## Calculate steady state for periodically driven systems
+
+See the docstring of [`steadystate_fourier`](@ref) for more details.
diff --git a/docs/src/users_guide/tensor.md b/docs/src/users_guide/tensor.md
index 23d89e0ad..fc0e6cdd0 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` ([StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl)), 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/docs/src/users_guide/time_evolution/brmesolve.md b/docs/src/users_guide/time_evolution/brmesolve.md
new file mode 100644
index 000000000..681463d60
--- /dev/null
+++ b/docs/src/users_guide/time_evolution/brmesolve.md
@@ -0,0 +1,231 @@
+# [Bloch-Redfield master equation](@id doc-TE:Bloch-Redfield-master-equation)
+
+The [Lindblad master equation](@ref doc-TE:Lindblad-Master-Equation-Solver) introduced earlier is constructed so that it describes a physical evolution of the density matrix (i.e., trace and positivity preserving), but it does not provide a connection to any underlying microscopic physical model. The Lindblad operators (collapse operators) describe phenomenological processes, such as for example dephasing and spin flips, and the rates of these processes are arbitrary parameters in the model. In many situations the collapse operators and their corresponding rates have clear physical interpretation, such as dephasing and relaxation rates, and in those cases the Lindblad master equation is usually the method of choice.
+
+However, in some cases, for example systems with varying energy biases and eigenstates and that couple to an environment in some well-defined manner (through a physically motivated system-environment interaction operator), it is often desirable to derive the master equation from more fundamental physical principles, and relate it to for example the noise-power spectrum of the environment.
+
+The Bloch-Redfield formalism is one such approach to derive a master equation from a microscopic system. It starts from a combined system-environment perspective, and derives a perturbative master equation for the system alone, under the assumption of weak system-environment coupling. One advantage of this approach is that the dissipation processes and rates are obtained directly from the properties of the environment. On the downside, it does not intrinsically guarantee that the resulting master equation unconditionally preserves the physical properties of the density matrix (because it is a perturbative method). The Bloch-Redfield master equation must therefore be used with care, and the assumptions made in the derivation must be honored. (The Lindblad master equation is in a sense more robust -- it always results in a physical density matrix -- although some collapse operators might not be physically justified). For a full derivation of the Bloch Redfield master equation, see e.g. [Cohen_Tannoudji_atomphoton](@citet) or [breuer2002](@citet). Here we present only a brief version of the derivation, with the intention of introducing the notation and how it relates to the implementation in `QuantumToolbox.jl`.
+
+
+## [Brief Derivation and Definitions](@id doc-TE:Brief-Derivation-and-Definitions)
+
+The starting point of the Bloch-Redfield formalism is the total Hamiltonian for the system and the environment (bath): ``\hat{H} = \hat{H}_{\rm S} + \hat{H}_{\rm B} + \hat{H}_{\rm I}``, where ``\hat{H}`` is the total system+bath Hamiltonian, ``\hat{H}_{\rm S}`` and ``\hat{H}_{\rm B}`` are the system and bath Hamiltonians, respectively, and ``\hat{H}_{\rm I}`` is the interaction Hamiltonian.
+
+The most general form of a master equation for the system dynamics is obtained by tracing out the bath from the von-Neumann equation of motion for the combined system (``\frac{d}{dt}\hat{\rho} = -i\hbar^{-1}[\hat{H}, \hat{\rho}]``). In the interaction picture the result is
+
+```math
+ \frac{d}{dt}\hat{\rho}_{\textrm{S}}(t) = - \hbar^{-2}\int_0^t d\tau\; \textrm{Tr}_{\textrm{B}} [\hat{H}_{\textrm{I}}(t), [\hat{H}_{\textrm{I}}(\tau), \hat{\rho}_{\textrm{S}}(\tau)\otimes\hat{\rho}_{\textrm{B}}]],
+```
+
+where the additional assumption that the total system-bath density matrix can be factorized as ``\hat{\rho}(t) \approx \hat{\rho}_{\textrm{S}}(t) \otimes \hat{\rho}_{\textrm{B}}``. This assumption is known as the Born approximation, and it implies that there never is any entanglement between the system and the bath, neither in the initial state nor at any time during the evolution. *It is justified for weak system-bath interaction.*
+
+The master equation above is non-Markovian, i.e., the change in the density matrix at a time ``t`` depends on states at all times ``\tau < t``, making it intractable to solve both theoretically and numerically. To make progress towards a manageable master equation, we now introduce the Markovian approximation, in which ``\hat{\rho}_{\textrm{S}}(\tau)`` is replaced by ``\hat{\rho}_{\textrm{S}}(t)``. The result is the Redfield equation
+
+```math
+ \frac{d}{dt}\hat{\rho}_{\textrm{S}}(t) = - \hbar^{-2}\int_0^t d\tau\; \textrm{Tr}_{\textrm{B}} [\hat{H}_{\textrm{I}}(t), [\hat{H}_{\textrm{I}}(\tau), \hat{\rho}_{\textrm{S}}(t)\otimes\hat{\rho}_{\textrm{B}}]],
+```
+
+which is local in time with respect the density matrix, but still not Markovian since it contains an implicit dependence on the initial state. By extending the integration to infinity and substituting ``\tau \rightarrow t-\tau``, a fully Markovian master equation is obtained:
+
+```math
+ \frac{d}{dt}\hat{\rho}_{\textrm{S}}(t) = - \hbar^{-2}\int_0^\infty d\tau\; \textrm{Tr}_{\textrm{B}} [\hat{H}_{\textrm{I}}(t), [\hat{H}_{\textrm{I}}(t-\tau), \hat{\rho}_{\textrm{S}}(t)\otimes\hat{\rho}_{\textrm{B}}]].
+```
+
+The two Markovian approximations introduced above are valid if the time-scale with which the system dynamics changes is large compared to the time-scale with which correlations in the bath decays (corresponding to a "short-memory" bath, which results in Markovian system dynamics).
+
+The Markovian master equation above is still on a too general form to be suitable for numerical implementation. We therefore assume that the system-bath interaction takes the form ``\hat{H}_{\textrm{I}} = \sum_\alpha \hat{A}_\alpha \otimes \hat{B}_\alpha`` and where ``\hat{A}_\alpha`` are system operators and ``\hat{B}_\alpha`` are bath operators. This allows us to write master equation in terms of system operators and bath correlation functions:
+
+```math
+\begin{split}\frac{d}{dt}\hat{\rho}_{\textrm{S}}(t) =
+-\hbar^{-2}
+\sum_{\alpha\beta}
+\int_0^\infty d\tau\;
+\left\{
+g_{\alpha\beta}(\tau) \left[\hat{A}_\alpha(t)\hat{A}_\beta(t-\tau)\hat{\rho}_{\textrm{S}}(t) - \hat{A}_\beta(t-\tau)\hat{\rho}_{\textrm{S}}(t)\hat{A}_\alpha(t)\right]
+\right. \nonumber\\
+\left.
++ g_{\alpha\beta}(-\tau) \left[\hat{\rho}_{\textrm{S}}(t)\hat{A}_\alpha(t-\tau)\hat{A}_\beta(t) - \hat{A}_\beta(t)\hat{\rho}_{\textrm{S}}(t)\hat{A}_\beta(t-\alpha)\right]
+\right\},\end{split}
+```
+
+where ``g_{\alpha\beta}(\tau) = \textrm{Tr}_{\textrm{B}}\left[\hat{B}_\alpha(t)\hat{B}_\beta(t-\tau)\hat{\rho}_{\textrm{B}}\right] = \langle\hat{B}_\alpha(\tau)\hat{B}_\beta(0)\rangle``, since the bath state ``\hat{\rho}_{\textrm{B}}`` is a steady state.
+
+In the eigenbasis of the system Hamiltonian, where ``A_{mn}(t) = A_{mn} e^{i\omega_{mn}t}``, ``\omega_{mn} = \omega_m - \omega_n`` and ``\omega_m`` are the eigenfrequencies corresponding to the eigenstate ``|m\rangle``, we obtain in matrix form in the Schrödinger picture
+
+```math
+\begin{aligned}
+ \frac{d}{dt} \rho_{ab}(t) = & -i\omega_{ab}\rho_{ab}(t)\\
+ &-\hbar^{-2} \sum_{\alpha,\beta} \sum_{c,d}^{\textrm{sec}} \int_0^\infty d\tau\;
+ \left\{
+ g_{\alpha\beta}(\tau)
+ \left[\delta_{bd}\sum_nA^\alpha_{an}A^\beta_{nc}e^{i\omega_{cn}\tau}
+ -
+ A^\beta_{ac} A^\alpha_{db} e^{i\omega_{ca}\tau}
+ \right]
+ \right. \\
+ &+
+ \left.
+ g_{\alpha\beta}(-\tau)
+ \left[\delta_{ac}\sum_n A^\alpha_{dn}A^\beta_{nb} e^{i\omega_{nd}\tau}
+ -
+ A^\beta_{ac}A^\alpha_{db}e^{i\omega_{bd}\tau}
+ \right]
+ \right\} \rho_{cd}(t),
+\end{aligned}
+```
+
+where the "sec" above the summation symbol indicate summation of the secular terms which satisfy ``|\omega_{ab}-\omega_{cd}| \ll \tau_ {\rm decay}``. This is an almost-useful form of the master equation. The final step before arriving at the form of the Bloch-Redfield master equation that is implemented in `QuantumToolbox.jl`, involves rewriting the bath correlation function ``g(\tau)`` in terms of the noise-power spectrum of the environment ``S(\omega) = \int_{-\infty}^\infty d\tau e^{i\omega\tau} g(\tau)``:
+
+```math
+ \int_0^\infty d\tau\; g_{\alpha\beta}(\tau) e^{i\omega\tau} = \frac{1}{2}S_{\alpha\beta}(\omega) + i\lambda_{\alpha\beta}(\omega),
+```
+
+where ``\lambda_{ab}(\omega)`` is an energy shift that is neglected here. The final form of the Bloch-Redfield master equation is
+
+```math
+ \frac{d}{dt}\rho_{ab}(t)
+ =
+ -i\omega_{ab}\rho_{ab}(t)
+ +
+ \sum_{c,d}^{\textrm{sec}}R_{abcd}\rho_{cd}(t),
+```
+
+where
+
+```math
+\begin{aligned}
+ R_{abcd} = -\frac{\hbar^{-2}}{2} \sum_{\alpha,\beta}
+ \left\{
+ \delta_{bd}\sum_nA^\alpha_{an}A^\beta_{nc}S_{\alpha\beta}(\omega_{cn})
+ -
+ A^\beta_{ac} A^\alpha_{db} S_{\alpha\beta}(\omega_{ca})
+ \right. \nonumber\\
+ +
+ \left.
+ \delta_{ac}\sum_n A^\alpha_{dn}A^\beta_{nb} S_{\alpha\beta}(\omega_{dn})
+ -
+ A^\beta_{ac}A^\alpha_{db} S_{\alpha\beta}(\omega_{db})
+ \right\},
+\end{aligned}
+```
+
+is the Bloch-Redfield tensor.
+
+The Bloch-Redfield master equation in this form is suitable for numerical implementation. The input parameters are the system Hamiltonian ``\hat{H}_{\textrm{S}}``, the system operators through which the environment couples to the system ``\hat{A}_\alpha``, and the noise-power spectrum ``S_{\alpha\beta}(\omega)`` associated with each system-environment interaction term.
+
+To simplify the numerical implementation we often assume that ``\hat{A}_\alpha`` are Hermitian and that cross-correlations between different environment operators vanish, resulting in
+
+```math
+\begin{aligned}
+ R_{abcd} = -\frac{\hbar^{-2}}{2} \sum_{\alpha}
+ \left\{
+ \delta_{bd}\sum_nA^\alpha_{an}A^\alpha_{nc}S_{\alpha}(\omega_{cn})
+ -
+ A^\alpha_{ac} A^\alpha_{db} S_{\alpha}(\omega_{ca})
+ \right. \nonumber\\
+ +
+ \left.
+ \delta_{ac}\sum_n A^\alpha_{dn}A^\alpha_{nb} S_{\alpha}(\omega_{dn})
+ -
+ A^\alpha_{ac}A^\alpha_{db} S_{\alpha}(\omega_{db})
+ \right\}.
+\end{aligned}
+```
+
+## [Bloch-Redfield master equation in `QuantumToolbox.jl`](@id Bloch-Redfield-master-equation-in-QuantumToolbox-jl)
+
+### Preparing the Bloch-Redfield tensor
+
+In `QuantumToolbox.jl`, the Bloch-redfield master equation can be calculated using the function [`bloch_redfield_tensor`](@ref). It takes two mandatory arguments: The system Hamiltonian ``\hat{H}`` and a nested list `a_ops` consist of tuples as `(A, spec)` with `A::QuantumObject` being the [`Operator`](@ref) ``\hat{A}_\alpha`` and `spec::Function` being the spectral density function ``S_\alpha(\omega)``.
+
+It is possible to also get the ``\alpha``-th term for the bath directly using [`brterm`](@ref). This function takes only one Hermitian coupling operator ``\hat{A}_\alpha`` and spectral response function ``S_\alpha(\omega)``.
+
+To illustrate how to calculate the Bloch-Redfield tensor, let's consider a two-level atom
+
+```math
+ \hat{H}_{\textrm{S}} = -\frac{1}{2}\Delta\hat{\sigma}_x - \frac{1}{2}\varepsilon_0\hat{\sigma}_z
+```
+
+```@setup brmesolve
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+```@example brmesolve
+Δ = 0.2 * 2π
+ε0 = 1.0 * 2π
+γ1 = 0.5
+
+H = -Δ/2.0 * sigmax() - ε0/2 * sigmaz()
+
+ohmic_spectrum(ω) = (ω == 0.0) ? γ1 : γ1 / 2 * (ω / (2 * π)) * (ω > 0.0)
+
+R, U = bloch_redfield_tensor(H, ((sigmax(), ohmic_spectrum), ))
+
+R
+```
+
+Note that it is also possible to add Lindblad dissipation superoperators in the Bloch-Refield tensor by passing the operators via the third argument `c_ops` like you would in the [`mesolve`](@ref) or [`mcsolve`](@ref) functions. For convenience, when the keyword argument `fock_basis = false`, the function [`bloch_redfield_tensor`](@ref) also returns the basis transformation operator `U`, the eigen vector matrix, since they are calculated in the process of generating the Bloch-Redfield tensor `R`, and the `U` are usually needed again later when transforming operators between the laboratory basis and the eigen basis. The tensor can be obtained in the laboratory basis by setting `fock_basis = true`, in that case, the transformation operator `U` is not returned.
+
+### Time evolution
+
+The evolution of a wave function or density matrix, according to the Bloch-Redfield master equation, can be calculated using the function [`mesolve`](@ref) with Bloch-Refield tensor `R` in the laboratory basis instead of a [`liouvillian`](@ref). For example, to evaluate the expectation values of the ``\hat{\sigma}_x``, ``\hat{\sigma}_y``, and ``\hat{\sigma}_z`` operators for the example above, we can use the following code:
+
+```@example brmesolve
+Δ = 0.2 * 2 * π
+ϵ0 = 1.0 * 2 * π
+γ1 = 0.5
+
+H = - Δ/2.0 * sigmax() - ϵ0/2.0 * sigmaz()
+
+ohmic_spectrum(ω) = (ω == 0.0) ? γ1 : γ1 / 2 * (ω / (2 * π)) * (ω > 0.0)
+
+a_ops = ((sigmax(), ohmic_spectrum),)
+R = bloch_redfield_tensor(H, a_ops; fock_basis = Val(true))
+
+e_ops = [sigmax(), sigmay(), sigmaz()]
+
+# same initial random ket state in QuTiP doc
+ψ0 = Qobj([
+ 0.05014193+0.66000276im,
+ 0.67231376+0.33147603im
+])
+
+tlist = LinRange(0, 15.0, 1000)
+sol = mesolve(R, ψ0, tlist, e_ops=e_ops)
+expt_list = real(sol.expect)
+
+# plot the evolution of state on Bloch sphere
+sphere = Bloch()
+add_points!(sphere, [expt_list[1,:], expt_list[2,:], expt_list[3,:]])
+sphere.vector_color = ["red"]
+
+add_vectors!(sphere, [Δ, 0, ϵ0] / √(Δ^2 + ϵ0^2))
+
+fig, _ = render(sphere)
+fig
+```
+
+The two steps of calculating the Bloch-Redfield tensor `R` and evolving according to the corresponding master equation can be combined into one by using the function [`brmesolve`](@ref), in addition to the same arguments as [`mesolve`](@ref) and [`mcsolve`](@ref), the nested list of operator-spectrum tuple should be given under `a_ops`.
+
+```@example brmesolve
+sol = brmesolve(H, ψ0, tlist, ((sigmax(),ohmic_spectrum),); e_ops=e_ops)
+```
+
+The resulting `sol` is of the `struct` [`TimeEvolutionSol`](@ref) as [`mesolve`](@ref).
+
+!!! note "Secular cutoff"
+ While the code example simulates the Bloch-Redfield equation in the secular approximation, `QuantumToolbox`'s implementation allows the user to simulate the non-secular version of the Bloch-Redfield equation by setting `sec_cutoff=-1`, as well as do a partial secular approximation by setting it to a `Float64` , this float number will become the cutoff for the summation (``\sum_{c,d}^{\textrm{sec}}``) in the previous equations, meaning that terms with ``\omega_{ab} - \omega_{cd}`` greater than the `sec_cutoff` will be neglected. Its default value is `0.1` which corresponds to the secular approximation.
+
+For example, the command
+
+```julia
+sol = brmesolve(H, ψ0, tlist, ((sigmax(),ohmic_spectrum),); e_ops=e_ops, sec_cutoff=-1)
+```
+
+will simulate the same example as above without the secular approximation.
+
+!!! warning "Secular cutoff"
+ Using the non-secular version may lead to negativity issues.
diff --git a/docs/src/users_guide/time_evolution/intro.md b/docs/src/users_guide/time_evolution/intro.md
index fe0097ff0..ae716173e 100644
--- a/docs/src/users_guide/time_evolution/intro.md
+++ b/docs/src/users_guide/time_evolution/intro.md
@@ -1,3 +1,54 @@
# [Time Evolution and Quantum System Dynamics](@id doc:Time-Evolution-and-Quantum-System-Dynamics)
-This page is still under construction, please visit [API](@ref doc-API) first.
\ No newline at end of file
+**Table of contents**
+
+- [Introduction](@ref doc-TE:Introduction)
+- [Time Evolution Solutions](@ref doc-TE:Time-Evolution-Solutions)
+ - [Solution](@ref doc-TE:Solution)
+ - [Accessing data in solutions](@ref doc-TE:Accessing-data-in-solutions)
+ - [Multiple trajectories solution](@ref doc-TE:Multiple-trajectories-solution)
+- [Schrödinger Equation Solver](@ref doc-TE:Schrödinger-Equation-Solver)
+ - [Unitary evolution](@ref doc-TE:Unitary-evolution)
+ - [Example: Spin dynamics](@ref doc-TE:Example:Spin-dynamics)
+- [Lindblad Master Equation Solver](@ref doc-TE:Lindblad-Master-Equation-Solver)
+ - [Von Neumann equation](@ref doc-TE:Von-Neumann-equation)
+ - [The Lindblad master equation](@ref doc-TE:The-Lindblad-master-equation)
+ - [Example: Dissipative Spin dynamics](@ref doc-TE:Example:Dissipative-Spin-dynamics)
+ - [Example: Harmonic oscillator in thermal bath](@ref doc-TE:Example:Harmonic-oscillator-in-thermal-bath)
+ - [Example: Two-level atom coupled to dissipative single-mode cavity](@ref doc-TE:Example:Two-level-atom-coupled-to-dissipative-single-mode-cavity)
+- [Monte Carlo Solver](@ref doc-TE:Monte-Carlo-Solver)
+ - [Monte Carlo wave-function](@ref doc-TE:Monte-Carlo-wave-function)
+ - [Example: Two-level atom coupled to dissipative single-mode cavity (MC)](@ref doc-TE:Example:Two-level-atom-coupled-to-dissipative-single-mode-cavity-(MC))
+ - [Running trajectories in parallel](@ref doc-TE:Running-trajectories-in-parallel)
+- [Stochastic Solver](@ref doc-TE:Stochastic-Solver)
+ - [Stochastic Schrödinger equation](@ref doc-TE:Stochastic-Schrödinger-equation)
+ - [Stochastic master equation](@ref doc-TE:Stochastic-master-equation)
+ - [Example: Homodyne detection](@ref doc-TE:Example:Homodyne-detection)
+- [Solving Problems with Time-dependent Hamiltonians](@ref doc-TE:Solving-Problems-with-Time-dependent-Hamiltonians)
+ - [Generate QobjEvo](@ref doc-TE:Generate-QobjEvo)
+ - [QobjEvo fields (attributes)](@ref doc-TE:QobjEvo-fields-(attributes))
+ - [Using parameters](@ref doc-TE:Using-parameters)
+- [Bloch-Redfield master equation](@ref doc-TE:Bloch-Redfield-master-equation)
+ - [Brief Derivation and Definitions](@ref doc-TE:Brief-Derivation-and-Definitions)
+ - [Bloch-Redfield master equation in `QuantumToolbox.jl`](@ref Bloch-Redfield-master-equation-in-QuantumToolbox-jl)
+
+
+# [Introduction](@id doc-TE:Introduction)
+
+Although in some cases, we want to find the stationary states of a quantum system, often we are interested in the dynamics: how the state of a system or an ensemble of systems evolves with time. `QuantumToolbox` provides many ways to model dynamics.
+
+There are two kinds of quantum systems: open systems that interact with a larger environment and closed systems that do not. In a closed system, the state can be described by a state vector. When we are modeling an open system, or an ensemble of systems, the use of the density matrix is mandatory.
+
+The following table lists the solvers provided by `QuantumToolbox` for dynamic quantum systems and the corresponding type of solution returned by the solver:
+
+| **Equation** | **Function Call** | **Problem** | **Returned Solution** |
+|:-------------|:------------------|:------------|:----------------------|
+| Unitary evolution, Schrödinger equation | [`sesolve`](@ref) | [`sesolveProblem`](@ref) | [`TimeEvolutionSol`](@ref) |
+| Lindblad master eqn. or Von Neuman eqn.| [`mesolve`](@ref) | [`mesolveProblem`](@ref) | [`TimeEvolutionSol`](@ref) |
+| Monte Carlo evolution | [`mcsolve`](@ref) | [`mcsolveProblem`](@ref) [`mcsolveEnsembleProblem`](@ref) | [`TimeEvolutionMCSol`](@ref) |
+| Stochastic Schrödinger equation | [`ssesolve`](@ref) | [`ssesolveProblem`](@ref) [`ssesolveEnsembleProblem`](@ref) | [`TimeEvolutionStochasticSol`](@ref) |
+| Stochastic master equation | [`smesolve`](@ref) | [`smesolveProblem`](@ref) [`smesolveEnsembleProblem`](@ref) | [`TimeEvolutionStochasticSol`](@ref) |
+| Bloch-Redfield master equation | [`brmesolve`](@ref) | - | [`TimeEvolutionSol`](@ref) |
+
+!!! note "Solving dynamics with pre-defined problems"
+ `QuantumToolbox` provides two different methods to solve the dynamics. One can use the function calls listed above by either taking all the operators (like Hamiltonian and collapse operators, etc.) as inputs directly, or generating the `prob`lems by yourself and take it as an input of the function call, e.g., `sesolve(prob)`.
diff --git a/docs/src/users_guide/time_evolution/mcsolve.md b/docs/src/users_guide/time_evolution/mcsolve.md
new file mode 100644
index 000000000..4f9aaa723
--- /dev/null
+++ b/docs/src/users_guide/time_evolution/mcsolve.md
@@ -0,0 +1,134 @@
+# [Monte Carlo Solver](@id doc-TE:Monte-Carlo-Solver)
+
+## [Monte Carlo wave-function](@id doc-TE:Monte-Carlo-wave-function)
+
+Where as the density matrix formalism describes the ensemble average over many identical realizations of a quantum system, the Monte Carlo (MC), or quantum-jump approach to wave function evolution, allows for simulating an individual realization of the system dynamics. Here, the environment is continuously monitored, resulting in a series of quantum jumps in the system wave function, conditioned on the increase in information gained about the state of the system via the environmental measurements. In general, this evolution is governed by the Schrödinger equation with a non-Hermitian effective Hamiltonian
+
+```math
+\hat{H}_{\textrm{eff}} = \hat{H} - \frac{i}{2} \sum_{n=1}^N \hat{C}_n^\dagger \hat{C}_n.
+```
+
+where ``\hat{H}`` is the system Hamiltonian and ``\hat{C}_n`` are collapse (jump) operators (assume ``N`` is the total number of collapse operators). Each collapse operator corresponds to a separate irreversible process with rate ``\gamma_n``. Here, the strictly negative non-Hermitian portion of the above equation gives rise to a reduction in the norm of the wave function, that to first-order in a small time ``\delta t``, is given by
+
+```math
+\langle \psi(t + \delta t) | \psi(t + \delta t) \rangle = 1 - \delta p,
+```
+
+where
+
+```math
+\delta p = \delta t \sum_{n=1}^N \langle \psi(t) | \hat{C}_n^\dagger \hat{C}_n | \psi(t) \rangle,
+```
+
+and ``\delta t`` is such that ``\delta p \ll 1``. With a probability of remaining in the state ``| \psi(t + \delta t) \rangle`` given by ``1 - \delta p``, the corresponding quantum jump probability is thus ``\delta p``. If the environmental measurements register a quantum jump, say via the emission of a photon into the environment, or a change in the spin of a quantum dot, the wave function undergoes a jump into a state defined by projecting ``| \psi(t) \rangle`` using the collapse operator ``\hat{C}_n`` corresponding to the measurement
+
+```math
+| \psi(t+\delta t) \rangle = \frac{\hat{C}_n |\psi(t)\rangle}{ \sqrt{\langle \psi(t) | \hat{C}_n^\dagger \hat{C}_n | \psi(t) \rangle} }.
+```
+
+If more than a single collapse operator is present in ``\hat{H}_{\textrm{eff}}``, the probability of collapse due to the ``n``-th operator ``\hat{C}_n`` is given by
+
+```math
+P_n(t) = \frac{1}{\delta p}\langle \psi(t) | \hat{C}_n^\dagger \hat{C}_n | \psi(t) \rangle.
+```
+
+Note that the probability of all collapses should be normalized to unity for all time ``t``, namely
+
+```math
+\sum_{n=1}^N P_n(t) = 1 ~~~\forall~~t.
+```
+
+Evaluating the MC evolution to first-order in time is quite tedious. Instead, `QuantumToolbox.jl` provides the function [`mcsolve`](@ref) which uses the following algorithm to simulate a single realization of a quantum system. Starting from a pure state ``| \psi(0) \rangle``:
+
+1. Choose two random numbers (``r_1`` and ``r_2``) between 0 and 1, where ``r_1`` represents the probability that a quantum jump occurs and ``r_2`` is used to select which collapse operator was responsible for the jump.
+1. Integrate the Schrödinger equation with respect to the effective Hamiltonian ``\hat{H}_{\textrm{eff}}`` until a time ``\tau`` such that the norm of the wave function satisfies ``\langle \psi(\tau) | \psi(\tau) \rangle = r_1``, at which point a jump occurs
+1. The resultant jump projects the system at time ``\tau`` into one of the renormalized states ``| \psi(\tau + \delta t) \rangle``. The corresponding collapse operator ``\hat{C}_n`` is chosen such that ``\tilde{n} \leq N`` is the smallest integer satisfying ``\sum_{n=1}^{\tilde{n}} P_n(\tau) \geq r_2``.
+1. Using the renormalized state from previous step as the new initial condition at time ``\tau`` and repeat the above procedure until the final simulation time is reached.
+
+## [Example: Two-level atom coupled to dissipative single-mode cavity (MC)](@id doc-TE:Example:Two-level-atom-coupled-to-dissipative-single-mode-cavity-(MC))
+
+In `QuantumToolbox.jl`, Monte Carlo evolution is implemented with the [`mcsolve`](@ref) function. It takes nearly the same arguments as the [`mesolve`](@ref) function for [Lindblad master equation evolution](@ref doc-TE:Lindblad-Master-Equation-Solver), except that the initial state must be a [`Ket`](@ref) vector, as oppose to a density matrix, and there is an optional keyword argument `ntraj` that defines the number of stochastic trajectories to be simulated. By default, `ntraj=500` indicating that `500` Monte Carlo trajectories will be performed.
+
+To illustrate the use of the Monte Carlo evolution of quantum systems in `QuantumToolbox.jl`, let’s again consider the case of a two-level atom coupled to a leaky cavity. The only differences to the master equation treatment is that in this case we invoke the [`mcsolve`](@ref) function instead of [`mesolve`](@ref)
+
+```@setup mcsolve
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+```@example mcsolve
+tlist = LinRange(0.0, 10.0, 200)
+
+ψ0 = tensor(fock(2, 0), fock(10, 8))
+a = tensor(qeye(2), destroy(10))
+σm = tensor(destroy(2), qeye(10))
+H = 2 * π * a' * a + 2 * π * σm' * σm + 2 * π * 0.25 * (σm * a' + σm' * a)
+
+c_ops = [sqrt(0.1) * a]
+e_ops = [a' * a, σm' * σm]
+
+sol_500 = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops)
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1],
+ xlabel = "Time",
+ ylabel = "Expectation values",
+ title = "Monte Carlo time evolution (500 trajectories)",
+)
+lines!(ax, tlist, real(sol_500.expect[1,:]), label = "cavity photon number", linestyle = :solid)
+lines!(ax, tlist, real(sol_500.expect[2,:]), label = "atom excitation probability", linestyle = :dash)
+
+axislegend(ax, position = :rt)
+
+fig
+```
+
+The advantage of the Monte Carlo method over the master equation approach is that only the state vector ([`Ket`](@ref)) is required to be kept in the computers memory, as opposed to the entire density matrix. For large quantum system this becomes a significant advantage, and the Monte Carlo solver is therefore generally recommended for such systems. However, for small systems, the added overhead of averaging a large number of stochastic trajectories to obtain the open system dynamics, as well as starting the multiprocessing functionality, outweighs the benefit of the minor (in this case) memory saving. Master equation methods are therefore generally more efficient when Hilbert space sizes are on the order of a couple of hundred states or smaller.
+
+We can also change the number of trajectories (`ntraj`). This can be used to explore the convergence of the Monte Carlo solver. For example, the following code plots the expectation values for `1`, `10` and `100` trajectories:
+
+```@example mcsolve
+e_ops = [a' * a]
+
+sol_1 = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ntraj = 1)
+sol_10 = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ntraj = 10)
+sol_100 = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ntraj = 100)
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1],
+ xlabel = "Time",
+ ylabel = "Expectation values",
+ title = "Monte Carlo time evolution",
+)
+lines!(ax, tlist, real(sol_1.expect[1,:]), label = "1 trajectory", linestyle = :dashdot)
+lines!(ax, tlist, real(sol_10.expect[1,:]), label = "10 trajectories", linestyle = :dash)
+lines!(ax, tlist, real(sol_100.expect[1,:]), label = "100 trajectories", linestyle = :solid)
+
+axislegend(ax, position = :rt)
+
+fig
+```
+
+## [Running trajectories in parallel](@id doc-TE:Running-trajectories-in-parallel)
+
+Monte Carlo evolutions often need hundreds of trajectories to obtain sufficient statistics. Since all trajectories are independent of each other, they can be computed in parallel. The keyword argument `ensemblealg` can specify how the multiple trajectories are handled. The common ensemble methods are:
+
+- `EnsembleSerial()`: No parallelism
+- `EnsembleThreads()`: **The default.** This uses multithreading.
+- `EnsembleDistributed()`: This uses as many processors as you have Julia processes.
+- `EnsembleSplitThreads()`: This uses multithreading on each process.
+
+!!! note "Other Ensemble Algorithms"
+ See the [documentation of `DifferentialEquations.jl`](https://docs.sciml.ai/DiffEqDocs/stable/features/ensemble/) for more details. Also, see Julia's documentation for more details about multithreading and adding more processes.
+
+```julia
+sol_serial = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ensemblealg=EnsembleSerial())
+sol_parallel = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ensemblealg=EnsembleThreads());
+```
+
+!!! tip "Parallelization on a Cluster"
+ See the section [Intensive parallelization on a Cluster](@ref doc:Intensive-parallelization-on-a-Cluster) for more details.
diff --git a/docs/src/users_guide/time_evolution/mesolve.md b/docs/src/users_guide/time_evolution/mesolve.md
new file mode 100644
index 000000000..dc5b3eb13
--- /dev/null
+++ b/docs/src/users_guide/time_evolution/mesolve.md
@@ -0,0 +1,225 @@
+# [Lindblad Master Equation Solver](@id doc-TE:Lindblad-Master-Equation-Solver)
+
+```@setup mesolve
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+## [Von Neumann equation](@id doc-TE:Von-Neumann-equation)
+
+While the evolution of the state vector in a closed quantum system is deterministic (as we discussed in the previous section: [Schrödinger Equation Solver](@ref doc-TE:Schrödinger-Equation-Solver)), open quantum systems are stochastic in nature. The effect of an environment on the system of interest is to induce stochastic transitions between energy levels, and to introduce uncertainty in the phase difference between states of the system. The state of an open quantum system is therefore described in terms of ensemble averaged states using the density matrix formalism. A density matrix ``\hat{\rho}`` describes a probability distribution of quantum states ``|\psi_n\rangle`` in a matrix representation, namely
+
+```math
+\hat{\rho} = \sum_n p_n |\psi_n\rangle\langle\psi_n|,
+```
+
+where ``p_n`` is the classical probability that the system is in the quantum state ``|\psi_n\rangle``. The time evolution of a density matrix ``\hat{\rho}`` is the topic of the remaining portions of this section.
+
+The time evolution of the density matrix ``\hat{\rho}(t)`` under closed system dynamics is governed by the von Neumann equation:
+
+```math
+\begin{equation}
+\frac{d}{dt}\hat{\rho}(t) = -\frac{i}{\hbar}\left[\hat{H}, \hat{\rho}(t)\right],
+\end{equation}
+```
+
+where ``[\cdot, \cdot]`` represents the commutator. The above equation is equivalent to the Schrödinger equation described in the [previous section](@ref doc-TE:Schrödinger-Equation-Solver) under the density matrix formalism.
+
+In `QuantumToolbox`, given a Hamiltonian, we can calculate the unitary (non-dissipative) time-evolution of an arbitrary initial state using the `QuantumToolbox` time evolution problem [`mesolveProblem`](@ref) or directly call the function [`mesolve`](@ref). It evolves the density matrix ``\hat{\rho}(t)`` and evaluates the expectation values for a set of operators `e_ops` at each given time points, using an ordinary differential equation solver provided by the powerful julia package [`DifferentialEquation.jl`](https://docs.sciml.ai/DiffEqDocs/stable/).
+
+```@example mesolve
+H = 0.5 * sigmax()
+state0 = basis(2, 0) # state vector
+tlist = LinRange(0.0, 10.0, 20)
+
+sol = mesolve(H, state0, tlist, e_ops = [sigmaz()])
+```
+
+!!! note "Use sesolve for improved efficiency"
+ Here, if the Hamiltonian `H` is given as an [`Operator`](@ref), and the initial state `state0` is given as a state vector ``|\psi(0)\rangle`` (in the type of [`Ket`](@ref)), it will automatically call [`sesolve`](@ref) for improved efficiency.
+
+The function returns [`TimeEvolutionSol`](@ref), as described in the previous section [Time Evolution Solutions](@ref doc-TE:Time-Evolution-Solutions).
+
+```@example mesolve
+sol.states
+```
+
+Here, only the final state is stored because the `states` will be saved depend on the keyword argument `saveat` in `kwargs`. If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state).
+
+One can also specify `e_ops` and `saveat` separately:
+
+```@example mesolve
+tlist = [0, 5, 10]
+state0 = ket2dm(basis(2, 0)) # density matrix
+sol = mesolve(H, state0, tlist, e_ops = [sigmay()], saveat = tlist)
+```
+
+```@example mesolve
+sol.expect
+```
+
+```@example mesolve
+sol.states
+```
+
+Note that when the initial state `state0` is given as a density matrix ``|\psi(0)\rangle\langle\psi(0)|`` (in the type of [`Operator`](@ref)), the stored `states` will also be in the type of [`Operator`](@ref) (density matrix).
+
+## [The Lindblad master equation](@id doc-TE:The-Lindblad-master-equation)
+
+The standard approach for deriving the equations of motion for a system interacting with its environment is to expand the scope of the system to include the environment. The combined quantum system is then closed, and its evolution is also governed by the von Neumann equation
+
+```math
+\begin{equation}
+\frac{d}{dt}\hat{\rho}_{\textrm{tot}}(t) = -\frac{i}{\hbar}\left[\hat{H}_{\textrm{tot}}, \hat{\rho}_{\textrm{tot}}(t)\right].
+\end{equation}
+```
+
+Here, the total Hamiltonian
+
+```math
+\hat{H}_{\textrm{tot}} = \hat{H}_{\textrm{sys}} + \hat{H}_{\textrm{env}} + \hat{H}_{\textrm{int}},
+```
+
+includes the original system Hamiltonian ``\hat{H}_{\textrm{sys}}``, the Hamiltonian for the environment ``\hat{H}_{\textrm{env}}``, and a term representing the interaction between the system and its environment ``\hat{H}_{\textrm{int}}``. Since we are only interested in the dynamics of the system, we can, perform a partial trace over the environmental degrees of freedom, and thereby obtain a master equation for the motion of the original system density matrix ``\hat{\rho}_{\textrm{sys}}(t)=\textrm{Tr}_{\textrm{env}}[\hat{\rho}_{\textrm{tot}}(t)]``. The most general trace-preserving and completely positive form of this evolution is the Lindblad master equation for the reduced density matrix, namely
+
+```math
+\begin{equation}
+\frac{d}{dt}\hat{\rho}_{\textrm{sys}}(t) = -\frac{i}{\hbar}\left[\hat{H}_{\textrm{sys}}, \hat{\rho}_{\textrm{sys}}(t)\right] + \sum_n \hat{C}_n \hat{\rho}_{\textrm{sys}}(t) \hat{C}_n^\dagger - \frac{1}{2} \hat{C}_n^\dagger \hat{C}_n \hat{\rho}_{\textrm{sys}}(t) - \frac{1}{2} \hat{\rho}_{\textrm{sys}}(t) \hat{C}_n^\dagger \hat{C}_n
+\end{equation}
+```
+
+where ``\hat{C}_n \equiv \sqrt{\gamma_n}\hat{A}_n`` are the collapse operators, ``\hat{A}_n`` are the operators acting on the system in ``\hat{H}_{\textrm{int}}`` which describes the system-environment interaction, and ``\gamma_n`` are the corresponding rates. The derivation of Lindblad master equation may be found in several sources, and will not be reproduced here. Instead, we emphasize the approximations that are required to arrive at the above Lindblad master equation from physical arguments, and hence perform a calculation in `QuantumToolbox`:
+
+- **Separability:** At ``t = 0``, there are no correlations between the system and environment, such that the total density matrix can be written as a tensor product, namely ``\hat{\rho}_{\textrm{tot}}(0)=\hat{\rho}_{\textrm{sys}}(0)\otimes\hat{\rho}_{\textrm{env}}(0)``.
+- **Born approximation:** Requires: (i) the state of the environment does not significantly change as a result of the interaction with the system; (ii) the system and the environment remain separable throughout the evolution. These assumptions are justified if the interaction is weak, and if the environment is much larger than the system. In summary, ``\hat{\rho}_{\textrm{tot}}(t)\approx\hat{\rho}_{\textrm{sys}}(t)\otimes\hat{\rho}_{\textrm{env}}(0)``.
+- **Markov approximation:** The time-scale of decay for the environment ``\tau_{\textrm{env}}`` is much shorter than the smallest time-scale of the system dynamics, i.e., ``\tau_{\textrm{sys}}\gg\tau_{\textrm{env}}``. This approximation is often deemed a “short-memory environment” as it requires the environmental correlation functions decay in a fast time-scale compared to those of the system.
+- **Secular approximation:** Stipulates that elements in the master equation corresponding to transition frequencies satisfy ``|\omega_{ab}-\omega_{cd}| \ll 1/\tau_{\textrm{sys}}``, i.e., all fast rotating terms in the interaction picture can be neglected. It also ignores terms that lead to a small renormalization of the system energy levels. This approximation is not strictly necessary for all master-equation formalisms (e.g., the Block-Redfield master equation), but it is required for arriving at the Lindblad form in the above equation which is used in [`mesolve`](@ref).
+
+For systems with environments satisfying the conditions outlined above, the Lindblad master equation governs the time-evolution of the system density matrix, giving an ensemble average of the system dynamics. In order to ensure that these approximations are not violated, it is important that the decay rates ``\gamma_n`` be smaller than the minimum energy splitting in the system Hamiltonian. Situations that demand special attention therefore include, for example, systems strongly coupled to their environment, and systems with degenerate or nearly degenerate energy levels.
+
+What is new in the master equation compared to the Schrödinger equation (or von Neumann equation) are processes that describe dissipation in the quantum system due to its interaction with an environment. For example, evolution that includes incoherent processes such as relaxation and dephasing. These environmental interactions are defined by the operators ``\hat{A}_n`` through which the system couples to the environment, and rates ``\gamma_n`` that describe the strength of the processes.
+
+In `QuantumToolbox`, the function [`mesolve`](@ref) can also be used for solving the master equation. This is done by passing a list of collapse operators (`c_ops`) as the fourth argument of the [`mesolve`](@ref) function in order to define the dissipation processes of the Lindblad master equation. As we mentioned above, each collapse operator ``\hat{C}_n`` is the product of ``\sqrt{\gamma_n}`` (the square root of the rate) and ``\hat{A}_n`` (operator which describes the dissipation process).
+
+Furthermore, `QuantumToolbox` solves the master equation in the [`SuperOperator`](@ref) formalism. That is, a Liouvillian [`SuperOperator`](@ref) will be generated internally in [`mesolve`](@ref) by the input system Hamiltonian ``\hat{H}_{\textrm{sys}}`` and the collapse operators ``\hat{C}_n``. One can also generate the Liouvillian [`SuperOperator`](@ref) manually for special purposes, and pass it as the first argument of the [`mesolve`](@ref) function. To do so, it is useful to read the section [Superoperators and Vectorized Operators](@ref doc:Superoperators-and-Vectorized-Operators), and also the docstrings of the following functions:
+- [`spre`](@ref)
+- [`spost`](@ref)
+- [`sprepost`](@ref)
+- [`liouvillian`](@ref)
+- [`lindblad_dissipator`](@ref)
+
+## [Example: Dissipative Spin dynamics](@id doc-TE:Example:Dissipative-Spin-dynamics)
+
+Using the example with the dynamics of spin-``\frac{1}{2}`` from the previous section ([Schrödinger Equation Solver](@ref doc-TE:Schrödinger-Equation-Solver)), we can easily add a relaxation process (describing the dissipation of energy from the spin to the environment), by adding `[sqrt(γ) * sigmax()]` in the fourth parameter of the [`mesolve`](@ref) function.
+
+```@example mesolve
+H = 2 * π * 0.1 * sigmax()
+ψ0 = basis(2, 0) # spin-up
+tlist = LinRange(0.0, 10.0, 100)
+
+γ = 0.05
+c_ops = [sqrt(γ) * sigmax()]
+
+sol = mesolve(H, ψ0, tlist, c_ops, e_ops = [sigmaz(), sigmay()])
+```
+
+We can therefore plot the expectation values:
+
+```@example mesolve
+expt_z = real(sol.expect[1,:])
+expt_y = real(sol.expect[2,:])
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = "Time", ylabel = "Expectation values")
+lines!(ax, tlist, expt_z, label = L"\langle\hat{\sigma}_z\rangle", linestyle = :solid)
+lines!(ax, tlist, expt_y, label = L"\langle\hat{\sigma}_y\rangle", linestyle = :dash)
+
+axislegend(ax, position = :rt)
+
+fig
+```
+
+## [Example: Harmonic oscillator in thermal bath](@id doc-TE:Example:Harmonic-oscillator-in-thermal-bath)
+
+Consider a harmonic oscillator (single-mode cavity) couples to a thermal bath. If the single-mode cavity initially is in a `10`-photon [`fock`](@ref) state, the dynamics is calculated with the following code:
+
+```@example mesolve
+# Define parameters
+N = 20 # number of basis states to consider
+a = destroy(N)
+H = a' * a
+ψ0 = fock(N, 10) # initial state
+κ = 0.1 # coupling to oscillator
+n_th = 2 # temperature with average of 2 excitations
+tlist = LinRange(0, 50, 100)
+
+# collapse operators
+c_ops = [
+ sqrt(κ * (n_th + 1)) * a, # emission
+ sqrt(κ * n_th ) * a' # absorption
+]
+
+# find expectation value for particle number
+e_ops = [a' * a]
+
+sol = mesolve(H, ψ0, tlist, c_ops, e_ops=e_ops)
+Num = real(sol.expect[1, :])
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1],
+ title = L"Decay of Fock state $|10\rangle$ in a thermal environment with $\langle n\rangle=2$",
+ xlabel = "Time",
+ ylabel = "Number of excitations",
+)
+lines!(ax, tlist, Num)
+
+fig
+```
+
+## [Example: Two-level atom coupled to dissipative single-mode cavity](@id doc-TE:Example:Two-level-atom-coupled-to-dissipative-single-mode-cavity)
+
+Consider a two-level atom coupled to a dissipative single-mode cavity through a dipole-type interaction, which supports a coherent exchange of quanta between the two systems. If the atom initially is in its ground state and the cavity in a `5`-photon [`fock`](@ref) state, the dynamics is calculated with the following code:
+
+!!! note "Generate Liouviilian superoperator manually"
+ In this example, we demonstrate how to generate the Liouvillian [`SuperOperator`](@ref) manually and pass it as the first argument of the [`mesolve`](@ref) function.
+
+```@example mesolve
+# two-level atom
+σm = tensor(destroy(2), qeye(10))
+H_a = 2 * π * σm' * σm
+
+# dissipative single-mode cavity
+γ = 0.1 # dissipation rate
+a = tensor(qeye(2), destroy(10))
+H_c = 2 * π * a' * a
+c_ops = [sqrt(γ) * a]
+
+# coupling between two-level atom and single-mode cavity
+g = 0.25 # coupling strength
+H_I = 2 * π * g * (σm * a' + σm' * a)
+
+ψ0 = tensor(basis(2,0), fock(10, 5)) # initial state
+tlist = LinRange(0.0, 10.0, 200)
+
+# generate Liouvillian superoperator manually
+L = liouvillian(H_a + H_c + H_I, c_ops)
+sol = mesolve(L, ψ0, tlist, e_ops=[σm' * σm, a' * a])
+
+# expectation value of Number operator
+N_atom = real(sol.expect[1,:])
+N_cavity = real(sol.expect[2,:])
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = "Time", ylabel = "Expectation values")
+lines!(ax, tlist, N_atom, label = "atom excitation probability", linestyle = :solid)
+lines!(ax, tlist, N_cavity, label = "cavity photon number", linestyle = :dash)
+
+axislegend(ax, position = :rt)
+
+fig
+```
diff --git a/docs/src/users_guide/time_evolution/sesolve.md b/docs/src/users_guide/time_evolution/sesolve.md
new file mode 100644
index 000000000..7e4b3cb1e
--- /dev/null
+++ b/docs/src/users_guide/time_evolution/sesolve.md
@@ -0,0 +1,125 @@
+# [Schrödinger Equation Solver](@id doc-TE:Schrödinger-Equation-Solver)
+
+## [Unitary evolution](@id doc-TE:Unitary-evolution)
+
+The dynamics of a closed (pure) quantum system is governed by the Schrödinger equation
+
+```math
+i\hbar\frac{\partial}{\partial t}\Psi(\vec{x}, t) = \hat{H}\Psi(\vec{x}, t),
+```
+
+where ``\Psi(\vec{x}, t)`` is the wave function, ``\hat{H}`` is the Hamiltonian, and ``\hbar`` is reduced Planck constant. In general, the Schrödinger equation is a partial differential equation (PDE) where both
+``\Psi`` and ``\hat{H}`` are functions of space ``\vec{x}`` and time ``t``. For computational purposes it is useful to expand the PDE in a set of basis functions that span the Hilbert space of the Hamiltonian, and to write the equation in matrix and vector form, namely
+
+```math
+i\hbar\frac{d}{dt}|\psi(t)\rangle = \hat{H}|\psi(t)\rangle,
+```
+
+where ``|\psi(t)\rangle`` is the state vector, and the Hamiltonian ``\hat{H}`` is now under matrix representation. This matrix equation can, in principle, be solved by diagonalizing the Hamiltonian matrix ``\hat{H}``. In practice, however, it is difficult to perform this diagonalization unless the size of the Hilbert space (dimension of the matrix ``\hat{H}``) is small. Analytically, it is a formidable task to calculate the dynamics for systems with more than two states. If, in addition, we consider dissipation due to the inevitable interaction with a surrounding environment, the computational complexity grows even larger, and we have to resort to numerical calculations in all realistic situations. This illustrates the importance of numerical calculations in describing the dynamics of open quantum systems, and the need for efficient and accessible tools for this task.
+
+The Schrödinger equation, which governs the time-evolution of closed quantum systems, is defined by its Hamiltonian and state vector. In the previous sections, [Manipulating States and Operators](@ref doc:Manipulating-States-and-Operators) and [Tensor Products and Partial Traces](@ref doc:Tensor-products-and-Partial-Traces), we showed how Hamiltonians and state vectors are constructed in `QuantumToolbox.jl`. Given a Hamiltonian, we can calculate the unitary (non-dissipative) time-evolution of an arbitrary initial state vector ``|\psi(0)\rangle`` using the `QuantumToolbox` time evolution problem [`sesolveProblem`](@ref) or directly call the function [`sesolve`](@ref). It evolves the state vector ``|\psi(t)\rangle`` and evaluates the expectation values for a set of operators `e_ops` at each given time points, using an ordinary differential equation solver provided by the powerful julia package [`DifferentialEquation.jl`](https://docs.sciml.ai/DiffEqDocs/stable/).
+
+## [Example: Spin dynamics](@id doc-TE:Example:Spin-dynamics)
+
+```@setup sesolve
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+For example, the time evolution of a quantum spin-``\frac{1}{2}`` system (initialized in spin-``\uparrow``) with tunneling rate ``0.1`` is calculated, and the expectation values of the Pauli-Z operator ``\hat{\sigma}_z`` is also evaluated, with the following code
+
+```@example sesolve
+H = 2 * π * 0.1 * sigmax()
+ψ0 = basis(2, 0) # spin-up
+tlist = LinRange(0.0, 10.0, 20)
+
+prob = sesolveProblem(H, ψ0, tlist, e_ops = [sigmaz()])
+sol = sesolve(prob)
+```
+
+!!! note "Note"
+ Here, we generate the time evolution problem by [`sesolveProblem`](@ref) first and then put it into the function [`sesolve`](@ref). One can also directly call [`sesolve`](@ref), which we also demonstrates in below.
+
+The function returns [`TimeEvolutionSol`](@ref), as described in the previous section [Time Evolution Solutions](@ref doc-TE:Time-Evolution-Solutions). The attribute `expect` in `sol`ution is a list of expectation values for the operator(s) that are passed to the `e_ops` keyword argument.
+
+```@example sesolve
+sol.expect
+```
+
+Passing multiple operators to `e_ops` as a `Vector` results in the expectation values for each operators at each time points.
+
+```@example sesolve
+tlist = LinRange(0.0, 10.0, 100)
+sol = sesolve(H, ψ0, tlist, e_ops = [sigmaz(), sigmay()])
+```
+
+!!! note "Note"
+ Here, we call [`sesolve`](@ref) directly instead of pre-defining [`sesolveProblem`](@ref) first (as shown previously).
+
+```@example sesolve
+println(size(sol.times)) # time points corresponds to stored expectation values
+println(size(sol.times_states)) # time points corresponds to stored states
+```
+
+```@example sesolve
+expt = sol.expect
+print(size(expt))
+```
+
+We can therefore plot the expectation values:
+
+```@example sesolve
+expt_z = real(expt[1,:])
+expt_y = real(expt[2,:])
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = "Time", ylabel = "Expectation values")
+lines!(ax, tlist, expt_z, label = L"\langle\hat{\sigma}_z\rangle", linestyle = :solid)
+lines!(ax, tlist, expt_y, label = L"\langle\hat{\sigma}_y\rangle", linestyle = :dash)
+
+axislegend(ax, position = :rb)
+
+fig
+```
+
+If the keyword argument `e_ops` is not specified (or given as an empty `Vector`), the [`sesolve`](@ref) and functions return a [`TimeEvolutionSol`](@ref) that contains a list of state vectors which corresponds to the time points specified in `tlist`:
+
+```@example sesolve
+tlist = [0, 10]
+sol = sesolve(H, ψ0, tlist) # or specify: e_ops = []
+
+println(size(sol.times))
+println(size(sol.times_states))
+```
+
+```@example sesolve
+sol.states
+```
+
+This is because the `states` will be saved depend on the keyword argument `saveat` in `kwargs`. If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state).
+
+One can also specify `e_ops` and `saveat` separately:
+
+```@example sesolve
+tlist = [0, 5, 10]
+sol = sesolve(H, ψ0, tlist, e_ops = [sigmay()], saveat = tlist)
+```
+
+```@example sesolve
+print(size(sol.times))
+```
+
+```@example sesolve
+sol.expect
+```
+
+```@example sesolve
+print(size(sol.times_states))
+```
+
+```@example sesolve
+sol.states
+```
diff --git a/docs/src/users_guide/time_evolution/solution.md b/docs/src/users_guide/time_evolution/solution.md
new file mode 100644
index 000000000..e57d30362
--- /dev/null
+++ b/docs/src/users_guide/time_evolution/solution.md
@@ -0,0 +1,143 @@
+# [Time Evolution Solutions](@id doc-TE:Time-Evolution-Solutions)
+
+```@setup TE-solution
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+## [Solution](@id doc-TE:Solution)
+`QuantumToolbox` utilizes the powerful [`DifferentialEquation.jl`](https://docs.sciml.ai/DiffEqDocs/stable/) to simulate different kinds of quantum system dynamics. Thus, we will first look at the data structure used for returning the solution (`sol`) from [`DifferentialEquation.jl`](https://docs.sciml.ai/DiffEqDocs/stable/). The solution stores all the crucial data needed for analyzing and plotting the results of a simulation. A generic structure [`TimeEvolutionSol`](@ref) contains the following properties for storing simulation data:
+
+| **Fields (Attributes)** | **Description** |
+|:------------------------|:----------------|
+| `sol.times` | The list of time points at which the expectation values are calculated during the evolution. |
+| `sol.times_states` | The list of time points at which the states are stored during the evolution. |
+| `sol.states` | The list of result states corresponding to each time point in `sol.times_states`. |
+| `sol.expect` | The expectation values corresponding to each time point in `sol.times`. |
+| `sol.alg` | The algorithm which is used during the solving process. |
+| `sol.abstol` | The absolute tolerance which is used during the solving process. |
+| `sol.reltol` | The relative tolerance which is used during the solving process. |
+| `sol.retcode` (or `sol.converged`) | The returned status from the solver. |
+
+## [Accessing data in solutions](@id doc-TE:Accessing-data-in-solutions)
+
+To understand how to access the data in solution, we will use an example as a guide, although we do not worry about the simulation details at this stage. The Schrödinger equation solver ([`sesolve`](@ref)) used in this example returns [`TimeEvolutionSol`](@ref):
+
+```@example TE-solution
+H = 0.5 * sigmay()
+ψ0 = basis(2, 0)
+e_ops = [
+ proj(basis(2, 0)),
+ proj(basis(2, 1)),
+ basis(2, 0) * basis(2, 1)'
+]
+tlist = LinRange(0, 10, 100)
+sol = sesolve(H, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false))
+nothing # hide
+```
+
+To see what is contained inside the solution, we can use the `print` function:
+
+```@example TE-solution
+print(sol)
+```
+
+It tells us the number of expectation values are computed and the number of states are stored. Now we have all the information needed to analyze the simulation results. To access the data for the three expectation values, one can do:
+
+```@example TE-solution
+expt1 = real(sol.expect[1,:])
+expt2 = real(sol.expect[2,:])
+expt3 = real(sol.expect[3,:])
+nothing # hide
+```
+
+Recall that `Julia` uses `Fortran`-style indexing that begins with one (i.e., `[1,:]` represents the 1-st observable, where `:` represents all values corresponding to `tlist`).
+
+Together with the list of time points at which these expectation values are calculated:
+
+```@example TE-solution
+times = sol.times
+nothing # hide
+```
+
+we can plot the resulting expectation values:
+
+```@example TE-solution
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = L"t")
+lines!(ax, times, expt1, label = L"\langle 0 | \rho(t) | 0 \rangle")
+lines!(ax, times, expt2, label = L"\langle 1 | \rho(t) | 1 \rangle")
+lines!(ax, times, expt3, label = L"\langle 0 | \rho(t) | 1 \rangle")
+
+ylims!(ax, (-0.5, 1.0))
+axislegend(ax, position = :lb)
+
+fig
+```
+
+State vectors, or density matrices, are accessed in a similar manner:
+
+```@example TE-solution
+sol.states
+```
+
+Together with the list of time points at which these states are stored:
+
+```@example TE-solution
+times = sol.times_states
+nothing # hide
+```
+
+Here, the solution contains only one (final) state. Because the `states` will be saved depend on the keyword argument `saveat` in `kwargs`. If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). One can also specify `e_ops` and `saveat` separately.
+
+Some other solvers can have other output.
+
+## [Multiple trajectories solution](@id doc-TE:Multiple-trajectories-solution)
+
+The solutions are different for solvers which compute multiple trajectories, such as the [`TimeEvolutionMCSol`](@ref) (Monte Carlo) or the [`TimeEvolutionStochasticSol`](@ref) (stochastic methods). The storage of expectation values and states depends on the keyword argument `keep_runs_results`, which determines whether the results of all trajectories are stored in the solution.
+
+When the keyword argument `keep_runs_results` is passed as `Val(false)` to a multi-trajectory solver, the `states` and `expect` fields store only the average results (averaged over all trajectories). The results can be accessed by the following index-order:
+
+```julia
+sol.states[time_idx]
+sol.expect[e_op,time_idx]
+```
+
+For example:
+
+```@example TE-solution
+tlist = LinRange(0, 1, 11)
+c_ops = (destroy(2),)
+e_ops = (num(2),)
+
+sol_mc1 = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ntraj=25, keep_runs_results=Val(false), progress_bar=Val(false))
+
+size(sol_mc1.expect)
+```
+
+If the keyword argument `keep_runs_results = Val(true)`, the results for each trajectory and each time are stored, and the index-order of the elements in fields `states` and `expect` are:
+
+
+```julia
+sol.states[trajectory,time_idx]
+sol.expect[e_op,trajectory,time_idx]
+```
+
+For example:
+
+```@example TE-solution
+sol_mc2 = mcsolve(H, ψ0, tlist, c_ops, e_ops=e_ops, ntraj=25, keep_runs_results=Val(true), progress_bar=Val(false))
+
+size(sol_mc2.expect)
+```
+
+We also provide the following functions for statistical analysis of multi-trajectory `sol`utions:
+
+| **Functions** | **Description** |
+|:------------|:----------------|
+| [`average_states(sol)`](@ref average_states) | Return the trajectory-averaged result states (as density [`Operator`](@ref)) at each time point. |
+| [`average_expect(sol)`](@ref average_expect) | Return the trajectory-averaged expectation values at each time point. |
+| [`std_expect(sol)`](@ref std_expect) | Return the trajectory-wise standard deviation of the expectation values at each time point. |
diff --git a/docs/src/users_guide/time_evolution/stochastic.md b/docs/src/users_guide/time_evolution/stochastic.md
new file mode 100644
index 000000000..852ae089d
--- /dev/null
+++ b/docs/src/users_guide/time_evolution/stochastic.md
@@ -0,0 +1,156 @@
+# [Stochastic Solver](@id doc-TE:Stochastic-Solver)
+
+When a quantum system is subjected to continuous measurement, through homodyne detection for example, it is possible to simulate the conditional quantum state using stochastic Schrödinger and master equations. The solution of these stochastic equations are quantum trajectories, which represent the conditioned evolution of the system given a specific measurement record.
+
+## [Stochastic Schrödinger equation](@id doc-TE:Stochastic-Schrödinger-equation)
+
+The stochastic Schrödinger time evolution of a quantum system is defined by the following stochastic differential equation [Wiseman2009Quantum; section 4.4](@cite):
+
+```math
+d|\psi(t)\rangle = -i \hat{K} |\psi(t)\rangle dt + \sum_n \hat{M}_n |\psi(t)\rangle dW_n(t)
+```
+
+where
+
+```math
+\hat{K} = \hat{H} + i \sum_n \left(\frac{e_n}{2} \hat{S}_n - \frac{1}{2} \hat{S}_n^\dagger \hat{S}_n - \frac{e_n^2}{8}\right),
+```
+```math
+\hat{M}_n = \hat{S}_n - \frac{e_n}{2},
+```
+and
+```math
+e_n = \langle \psi(t) | \hat{S}_n + \hat{S}_n^\dagger | \psi(t) \rangle.
+```
+
+Above, ``\hat{H}`` is the Hamiltonian, ``\hat{S}_n`` are the stochastic collapse operators, and ``dW_n(t)`` is the real Wiener increment (associated to ``\hat{S}_n``) which has the expectation values of ``E[dW_n]=0`` and ``E[dW_n^2]=dt``.
+
+The solver [`ssesolve`](@ref) will construct the operators ``\hat{K}`` and ``\hat{M}_n``. Once the user passes the Hamiltonian (``\hat{H}``) and the stochastic collapse operators list (`sc_ops`; ``\{\hat{S}_n\}_n``). As with the [`mcsolve`](@ref), the number of trajectories and the random number generator for the noise realization can be fixed using the arguments: `ntraj` and `rng`, respectively.
+
+## [Stochastic master equation](@id doc-TE:Stochastic-master-equation)
+
+When the initial state of the system is a density matrix ``\rho(0)``, or when additional loss channels are included, the stochastic master equation solver [`smesolve`](@ref) must be used. The stochastic master equation is given by [Wiseman2009Quantum; section 4.4](@cite):
+
+```math
+d \rho (t) = -i [\hat{H}, \rho(t)] dt + \sum_i \mathcal{D}[\hat{C}_i] \rho(t) dt + \sum_n \mathcal{D}[\hat{S}_n] \rho(t) dt + \sum_n \mathcal{H}[\hat{S}_n] \rho(t) dW_n(t),
+```
+
+where
+
+```math
+\mathcal{D}[\hat{O}] \rho = \hat{O} \rho \hat{O}^\dagger - \frac{1}{2} \{\hat{O}^\dagger \hat{O}, \rho\},
+```
+
+is the Lindblad superoperator, and
+
+```math
+\mathcal{H}[\hat{O}] \rho = \hat{O} \rho + \rho \hat{O}^\dagger - \mathrm{Tr}[\hat{O} \rho + \rho \hat{O}^\dagger] \rho,
+```
+
+The above implementation takes into account 2 types of collapse operators. ``\hat{C}_i`` (`c_ops`) represent the collapse operators related to pure dissipation, while ``\hat{S}_n`` (`sc_ops`) are the stochastic collapse operators. The first three terms on the right-hand side of the above equation is the deterministic part of the evolution which takes into account all operators ``\hat{C}_i`` and ``\hat{S}_n``. The last term (``\mathcal{H}[\hat{S}_n] \rho(t)``) is the stochastic part given solely by the operators ``\hat{S}_n``.
+
+
+## [Example: Homodyne detection](@id doc-TE:Example:Homodyne-detection)
+
+First, we solve the dynamics for an optical cavity at absolute zero (``0K``) whose output is monitored using homodyne detection. The cavity decay rate is given by ``\kappa`` and the ``\Delta`` is the cavity detuning with respect to the driving field. The homodyne current ``J_x`` is calculated using
+
+```math
+J_x = \langle \hat{x} \rangle + dW/dt,
+```
+
+where ``\hat{x}`` is the operator build from the `sc_ops` as
+
+```math
+\hat{x} = \hat{S} + \hat{S}^\dagger
+```
+
+```@setup stochastic-solve
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+```@example stochastic-solve
+# parameters
+N = 20 # Fock space dimension
+Δ = 5 * 2 * π # cavity detuning
+κ = 2 # cavity decay rate
+α = 4 # intensity of initial state
+ntraj = 500 # number of trajectories
+
+tlist = 0:0.0025:1
+
+# operators
+a = destroy(N)
+x = a + a'
+H = Δ * a' * a
+
+# initial state
+ψ0 = coherent(N, √α)
+
+# temperature with average of 0 excitations (absolute zero)
+n_th = 0
+# c_ops = [√(κ * n_th) * a'] -> nothing
+sc_ops = [√(κ * (n_th + 1)) * a]
+```
+
+In this case, there is no additional dissipation (`c_ops = nothing`), and thus, we can use the [`ssesolve`](@ref):
+
+```@example stochastic-solve
+sse_sol = ssesolve(
+ H,
+ ψ0,
+ tlist,
+ sc_ops,
+ e_ops = [x],
+ ntraj = ntraj,
+ store_measurement = Val(true),
+)
+
+measurement_avg = sum(sse_sol.measurement, dims=2) / size(sse_sol.measurement, 2)
+measurement_avg = dropdims(measurement_avg, dims=2)
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = "Time")
+lines!(ax, tlist[1:end-1], real(measurement_avg[1,:]), label = L"J_x", color = :red, linestyle = :solid)
+lines!(ax, tlist, real(sse_sol.expect[1,:]), label = L"\langle x \rangle", color = :black, linestyle = :solid)
+
+axislegend(ax, position = :rt)
+
+fig
+```
+
+Next, we consider the same model but at a finite temperature to demonstrate [`smesolve`](@ref):
+
+```@example stochastic-solve
+# temperature with average of 1 excitations
+n_th = 1
+c_ops = [√(κ * n_th) * a']
+sc_ops = [√(κ * (n_th + 1)) * a]
+
+sme_sol = smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ sc_ops,
+ e_ops = [x],
+ ntraj = ntraj,
+ store_measurement = Val(true),
+)
+
+measurement_avg = sum(sme_sol.measurement, dims=2) / size(sme_sol.measurement, 2)
+measurement_avg = dropdims(measurement_avg, dims=2)
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = "Time")
+lines!(ax, tlist[1:end-1], real(measurement_avg[1,:]), label = L"J_x", color = :red, linestyle = :solid)
+lines!(ax, tlist, real(sme_sol.expect[1,:]), label = L"\langle x \rangle", color = :black, linestyle = :solid)
+
+axislegend(ax, position = :rt)
+
+fig
+```
diff --git a/docs/src/users_guide/time_evolution/time_dependent.md b/docs/src/users_guide/time_evolution/time_dependent.md
new file mode 100644
index 000000000..5c5a0b111
--- /dev/null
+++ b/docs/src/users_guide/time_evolution/time_dependent.md
@@ -0,0 +1,353 @@
+# [Solving Problems with Time-dependent Hamiltonians](@id doc-TE:Solving-Problems-with-Time-dependent-Hamiltonians)
+
+```@setup QobjEvo
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+## [Generate QobjEvo](@id doc-TE:Generate-QobjEvo)
+
+In the previous examples of solving time evolution, we assumed that the systems under consideration were described by time-independent Hamiltonians. However, many systems have explicit time dependence in either the Hamiltonian, or the collapse operators (`c_ops`) describing coupling to the environment, and sometimes both components might depend on time. The time evolution solvers such as [`sesolve`](@ref), [`mesolve`](@ref), etc., are all capable of handling time-dependent Hamiltonians and collapse operators.
+
+`QuantumToolbox.jl` uses [`QuantumObjectEvolution`](@ref) (or the abbreviated synonym: [`QobjEvo`](@ref)) to represent time-dependent quantum operators, and its `data` field (attribute) takes advantage of [`SciMLOperators.jl`](https://docs.sciml.ai/SciMLOperators/stable/).
+
+To generate a time-dependent operator [`QobjEvo`](@ref), one needs to specify a time-independent [`Qobj`](@ref) together with a time-dependent coefficient function with the form `coef(p, t)`, namely
+
+```@example QobjEvo
+coef(p, t) = sin(t)
+
+H_t = QobjEvo(sigmax(), coef)
+```
+
+!!! warning "The inputs of coefficient function"
+ Please note that although we didn't use the argument `p` in the definition of `coef`, we still need to put a dummy input `p` (in front of `t`) in the declaration of `coef`. We will describe how to use the parameter `p` in the section [Using parameters](@ref doc-TE:Using-parameters).
+
+The [`QobjEvo`](@ref) can also be generated by specifying many pairs of time-independent [`Qobj`](@ref) and time-dependent coefficient function. For instance, we will look at a case with the total Hamiltonian ``\hat{H}(t)`` can be separate into time-independent part (``\hat{H}_0``) and a summation of many time-dependent operators, which takes the form:
+
+```math
+\hat{H}(t) = \hat{H}_0 + \sum_j f_j(t) \hat{H}_j.
+```
+
+The following code sets up this problem by using a `Tuple` which contains many time-dependent pairs `(H_j, f_j)`, namely
+
+```@example QobjEvo
+H0 = qeye(2)
+
+H1 = sigmax()
+f1(p, t) = sin(t)
+
+H2 = sigmay()
+f2(p, t) = cos(t)
+
+H3 = sigmaz()
+f3(p, t) = 9 * exp(-(t / 5)^2)
+
+H_tuple = (
+ H0,
+ (H1, f1),
+ (H2, f2),
+ (H3, f3),
+)
+
+H_t = QobjEvo(H_tuple)
+```
+
+This is equivalent by generating the [`QobjEvo`](@ref) separately and `+` (or `sum`) them up:
+
+```@example QobjEvo
+H_t == H0 + QobjEvo(H1, f1) + QobjEvo(H2, f2) + QobjEvo(H3, f3)
+```
+
+Most solvers will accept any format that could be made into a [`QobjEvo`](@ref) for the Hamiltonian. All of the followings are equivalent:
+
+```julia
+sol = mesolve(H_t, ...)
+sol = mesolve(H_tuple, ...)
+```
+
+Collapse operators `c_ops` only accept a list where each element is either a [`Qobj`](@ref) or a [`QobjEvo`](@ref). For example, in the following call:
+
+```julia
+γ1 = sqrt(0.1)
+γ2(p, t) = sqrt(sin(t))
+c_ops = (
+ γ1 * sigmaz(),
+ QobjEvo(sigmax() + sigmay(), γ2)
+)
+
+sol = mesolve(H_t, ..., c_ops, ...)
+```
+
+[`mesolve`](@ref) will see `2` collapse operators: ``\sqrt{0.1} \hat{\sigma}_z`` and ``\sqrt{\sin(t)} (\hat{\sigma}_x + \hat{\sigma}_y)``.
+
+As an example, we will look at a case with a time-dependent Hamiltonian of the form ``\hat{H}(t) = \hat{H}_0 + f_1(t) \hat{H}_1``, where ``f_1(t) = A \exp \left[ -(t / \sigma)^2 \right]`` is the time-dependent driving strength. The following code sets up the problem
+
+```@example QobjEvo
+N = 2 # Set where to truncate Fock state for cavity
+
+# basis states for Atom
+u = basis(3, 0) # u state
+e = basis(3, 1) # excited state
+g = basis(3, 2) # ground state
+
+# operators
+g_e = tensor(qeye(N), g * e') # |g> g
+ sqrt(5 * γ0 / 9) * u_e # 5/9 e -> u
+]
+
+tlist = LinRange(0, 4, 200) # Define time vector
+ψ0 = tensor(basis(N, 0), u) # Define initial state
+
+# Build observables
+e_ops = [
+ a' * a,
+ u_u,
+ g_g
+]
+
+# solve dynamics
+exp_me = mesolve(H_t, ψ0, tlist, c_ops; e_ops = e_ops, progress_bar = Val(false)).expect
+exp_mc = mcsolve(H_t, ψ0, tlist, c_ops; e_ops = e_ops, ntraj = 100, progress_bar = Val(false)).expect
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = L"Time $t$", ylabel = "expectation value")
+lines!(ax, tlist, real(exp_me[1,:]), label = L"$\hat{a}^\dagger \hat{a}$ (mesolve)", color = :red, linestyle = :solid)
+lines!(ax, tlist, real(exp_mc[1,:]), label = L"$\hat{a}^\dagger \hat{a}$ (mcsolve)", color = :red, linestyle = :dash)
+lines!(ax, tlist, real(exp_me[2,:]), label = L"$|u \rangle\langle u|$ (mesolve)", color = :green, linestyle = :solid)
+lines!(ax, tlist, real(exp_mc[2,:]), label = L"$|u \rangle\langle u|$ (mcsolve)", color = :green, linestyle = :dash)
+lines!(ax, tlist, real(exp_me[3,:]), label = L"$|g \rangle\langle g|$ (mesolve)", color = :blue, linestyle = :solid)
+lines!(ax, tlist, real(exp_mc[3,:]), label = L"$|g \rangle\langle g|$ (mcsolve)", color = :blue, linestyle = :dash)
+
+axislegend(ax, position = :rc)
+
+fig
+```
+
+The result from [`mesolve`](@ref) is identical to that shown in the examples, the [`mcsolve`](@ref) however will be noticeably off, suggesting we should increase the number of trajectories `ntraj = 100` for this example.
+
+In addition, we can also consider the decay of a simple Harmonic oscillator with time-varying decay rate ``\gamma_1(t)``
+
+```@example QobjEvo
+N = 10 # number of basis states
+a = destroy(N)
+H = a' * a
+
+ψ0 = basis(N, 9) # initial state
+
+γ0 = 0.5
+γ1(p, t) = sqrt(γ0 * exp(-t))
+c_ops_ti = [sqrt(γ0) * a] # time-independent collapse term
+c_ops_td = [QobjEvo(a, γ1)] # time-dependent collapse term
+
+e_ops = [a' * a]
+
+tlist = LinRange(0, 10, 100)
+exp_ti = mesolve(H, ψ0, tlist, c_ops_ti, e_ops = e_ops; progress_bar = Val(false)).expect[1,:]
+exp_td = mesolve(H, ψ0, tlist, c_ops_td, e_ops = e_ops; progress_bar = Val(false)).expect[1,:]
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = L"Time $t$", ylabel = L"\langle \hat{a}^\dagger \hat{a} \rangle")
+lines!(ax, tlist, real(exp_ti), label = L"\gamma_0", linestyle = :solid)
+lines!(ax, tlist, real(exp_td), label = L"\gamma_1(t) = \gamma_0 e^{-t}", linestyle = :dash)
+
+axislegend(ax, position = :rt)
+
+fig
+```
+
+## [QobjEvo fields (attributes)](@id doc-TE:QobjEvo-fields-(attributes))
+
+[`QuantumObjectEvolution`](@ref) as a time-dependent quantum system, as its main functionality creates a [`QuantumObject`](@ref) at a specified time ``t``:
+
+```@example QobjEvo
+coef(p, t) = sin(π * t)
+Ht = QobjEvo(sigmaz(), coef)
+
+Ht(0.25) # t = 0.25
+```
+
+[`QuantumObjectEvolution`](@ref) shares a lot of properties with the [`QuantumObject`](@ref):
+
+```@example QobjEvo
+Ht.data
+```
+
+```@example QobjEvo
+Ht.type
+```
+
+```@example QobjEvo
+Ht.dims
+```
+
+The properties of a [`QuantumObjectEvolution`](@ref) can also be retrieved using several functions with inputting [`QuantumObjectEvolution`](@ref):
+
+```@example QobjEvo
+size(Ht)
+```
+
+```@example QobjEvo
+shape(Ht) # synonym of size(Ht)
+```
+
+```@example QobjEvo
+length(Ht)
+```
+
+```@example QobjEvo
+eltype(Ht) # element type
+```
+
+```@example QobjEvo
+println(isket(Ht)) # ket
+println(isbra(Ht)) # bra
+println(isoper(Ht)) # operator
+println(isoperket(Ht)) # operator-ket
+println(isoperbra(Ht)) # operator-bra
+println(issuper(Ht)) # super operator
+println(isconstant(Ht)) # time-independent or not
+println(ishermitian(Ht)) # Hermitian
+println(isherm(Ht)) # synonym of ishermitian(Ht)
+println(issymmetric(Ht)) # symmetric
+println(isposdef(Ht)) # positive definite (and Hermitian)
+```
+
+[`QobjEvo`](@ref) follow the same mathematical operations rules as [`Qobj`](@ref). They can be added, subtracted and multiplied with scalar, [`Qobj`](@ref) and [`QobjEvo`](@ref). They also support [`adjoint`](@ref), [`transpose`](@ref), and [`conj`](@ref) methods, and can be used for [`SuperOperator`](@ref) transformation:
+
+```@example QobjEvo
+coef(p, t) = sin(π * t)
+Ht = QobjEvo(sigmaz(), coef)
+
+coef2(p, t) = exp(-t)
+c_op = QobjEvo(destroy(2), coef2)
+
+L1 = -1im * (spre(Ht) - spost(Ht'))
+L1 += lindblad_dissipator(c_op)
+```
+
+Or equivalently:
+```@example QobjEvo
+L2 = liouvillian(Ht, [c_op])
+```
+
+```@example QobjEvo
+t = rand()
+L1(t) == L2(t)
+```
+
+!!! note "Optimization for superoperator transformation"
+ Although the value of `L1` and `L2` here is equivalent, one can observe that the structure of `L2.data` is much simpler than `L1.data`, which means that it will be more efficient to solve the time evolution using `L2` instead of `L1`. Therefore, we recommend to use [`liouvillian`](@ref) or [`lindblad_dissipator`](@ref) to generate time-dependent [`SuperOperator`](@ref) because these functions have been optimized.
+
+## [Using parameters](@id doc-TE:Using-parameters)
+
+Until now, the coefficients were only functions of time `t`. In the definition of ``f_1(t) = A \exp \left[ -(t / \sigma)^2 \right]`` in the previous example, the driving amplitude ``A`` and width of the gaussian driving term ``\sigma`` were hardcoded with their numerical values. This is fine for problems that are specialized, or that we only want to run once. However, in many cases, we would like to study the same problem with a range of parameters and don't have to worry about manually changing the values on each run. `QuantumToolbox.jl` allows you to accomplish this by adding extra parameters `p` to coefficient functions that make the [`QobjEvo`](@ref). For instance, instead of explicitly writing `9` for ``A`` and `5` for ``\sigma``, we can either specify them with `p` as a `Vector` or `NamedTuple`:
+
+```@example QobjEvo
+# specify p as a Vector
+f1_v(p, t) = p[1] * exp(-(t / p[2])^2)
+
+H_t_v = QobjEvo(sigmaz(), f1_v)
+
+p_v = [9, 5] # 1st element represents A; 2nd element represents σ
+t = 1
+H_t_v(p_v, t)
+```
+
+```@example QobjEvo
+# specify p as a NamedTuple
+f1_n(p, t) = p.A * exp(-(t / p.σ)^2)
+
+H_t_n = QobjEvo(sigmaz(), f1_n)
+
+p_n = (A = 9, σ = 5)
+t = 1
+H_t_n(p_n, t)
+```
+
+```@example QobjEvo
+t = rand()
+H_t_v(p_v, t) == H_t_n(p_n, t)
+```
+
+!!! note "Custom structure of parameter"
+ For more advanced usage, any custom `struct` of parameter `p` can be used.
+
+When solving time evolutions, the solvers take an single keyword argument `params`, which will be directly passed to input `p` of the coefficient functions to build the time dependent Hamiltonian and collapse operators. For example, with `p::Vector`:
+
+```@example QobjEvo
+f1(p, t) = p[1] * cos(p[2] * t)
+f2(p, t) = p[3] * sin(p[4] * t)
+γ(p, t) = sqrt(p[5] * exp(-p[6] * t))
+p_total = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
+
+H_t = sigmaz() + QobjEvo(sigmax(), f1) + QobjEvo(sigmay(), f2)
+
+c_ops = [
+ QobjEvo(destroy(2), γ)
+]
+
+e_ops = [sigmaz()]
+
+ψ0 = basis(2, 0)
+tlist = LinRange(0, 10, 20)
+sol1 = mesolve(H_t, ψ0, tlist, c_ops, params = p_total, e_ops = e_ops; progress_bar = Val(false))
+```
+
+Similarly, with `p::NamedTuple`:
+
+```@example QobjEvo
+f1(p, t) = p.A1 * cos(p.ω1 * t)
+f2(p, t) = p.A2 * sin(p.ω2 * t)
+γ(p, t) = sqrt(p.A3 * exp(-p.γ0 * t))
+p_total = (
+ A1 = 0.1,
+ ω1 = 0.2,
+ A2 = 0.3,
+ ω2 = 0.4,
+ A3 = 0.5,
+ γ0 = 0.6
+)
+
+H_t = sigmaz() + QobjEvo(sigmax(), f1) + QobjEvo(sigmay(), f2)
+
+c_ops = [
+ QobjEvo(destroy(2), γ)
+]
+
+e_ops = [sigmaz()]
+
+ψ0 = basis(2, 0)
+tlist = LinRange(0, 10, 20)
+sol2 = mesolve(H_t, ψ0, tlist, c_ops, params = p_total, e_ops = e_ops; progress_bar = Val(false))
+```
+
+```@example QobjEvo
+sol1.expect == sol2.expect
+```
diff --git a/docs/src/users_guide/two_time_corr_func.md b/docs/src/users_guide/two_time_corr_func.md
new file mode 100644
index 000000000..ba592d7e3
--- /dev/null
+++ b/docs/src/users_guide/two_time_corr_func.md
@@ -0,0 +1,266 @@
+# [Two-time Correlation Functions](@id doc:Two-time-Correlation-Functions)
+
+## Introduction
+
+With the `QuantumToolbox.jl` time-evolution function [`mesolve`](@ref), a state vector ([`Ket`](@ref)) or density matrix ([`Operator`](@ref)) can be evolved from an initial state at ``t_0`` to an arbitrary time ``t``, namely
+
+```math
+\hat{\rho}(t) = \mathcal{G}(t, t_0)\{\hat{\rho}(t_0)\},
+```
+where ``\mathcal{G}(t, t_0)\{\cdot\}`` is the propagator defined by the equation of motion. The resulting density matrix can then be used to evaluate the expectation values of arbitrary combinations of same-time operators.
+
+To calculate two-time correlation functions on the form ``\left\langle \hat{A}(t+\tau) \hat{B}(t) \right\rangle``, we can use the quantum regression theorem [see, e.g., [Gardiner-Zoller2004](@citet)] to write
+
+```math
+\left\langle \hat{A}(t+\tau) \hat{B}(t) \right\rangle = \textrm{Tr} \left[\hat{A} \mathcal{G}(t+\tau, t)\{\hat{B}\hat{\rho}(t)\} \right] = \textrm{Tr} \left[\hat{A} \mathcal{G}(t+\tau, t)\{\hat{B} \mathcal{G}(t, 0)\{\hat{\rho}(0)\}\} \right],
+```
+
+We therefore first calculate ``\hat{\rho}(t) = \mathcal{G}(t, 0)\{\hat{\rho}(0)\}`` using [`mesolve`](@ref) with ``\hat{\rho}(0)`` as initial state, and then again use [`mesolve`](@ref) to calculate ``\mathcal{G}(t+\tau, t)\{\hat{B}\hat{\rho}(t)\}`` using ``\hat{B}\hat{\rho}(t)`` as initial state.
+
+Note that if the initial state is the steady state, then ``\hat{\rho}(t) = \mathcal{G}(t, 0)\{\hat{\rho}_{\textrm{ss}}\} = \hat{\rho}_{\textrm{ss}}`` and
+
+```math
+\left\langle \hat{A}(t+\tau) \hat{B}(t) \right\rangle = \textrm{Tr} \left[\hat{A} \mathcal{G}(t+\tau, t)\{\hat{B}\hat{\rho}_{\textrm{ss}}\} \right] = \textrm{Tr} \left[\hat{A} \mathcal{G}(\tau, 0)\{\hat{B} \hat{\rho}_{\textrm{ss}}\} \right] = \left\langle \hat{A}(\tau) \hat{B}(0) \right\rangle,
+```
+which is independent of ``t``, so that we only have one time coordinate ``\tau``.
+
+`QuantumToolbox.jl` provides a family of functions that assists in the process of calculating two-time correlation functions. The available functions and their usage is shown in the table below.
+
+| **Function call** | **Correlation function** |
+|:------------------|:-------------------------|
+| [`correlation_2op_2t`](@ref) | ``\left\langle \hat{A}(t + \tau) \hat{B}(t) \right\rangle`` or ``\left\langle \hat{A}(t) \hat{B}(t + \tau) \right\rangle`` |
+| [`correlation_2op_1t`](@ref) | ``\left\langle \hat{A}(\tau) \hat{B}(0) \right\rangle`` or ``\left\langle \hat{A}(0) \hat{B}(\tau) \right\rangle`` |
+| [`correlation_3op_1t`](@ref) | ``\left\langle \hat{A}(0) \hat{B}(\tau) \hat{C}(0) \right\rangle`` |
+| [`correlation_3op_2t`](@ref) | ``\left\langle \hat{A}(t) \hat{B}(t + \tau) \hat{C}(t) \right\rangle`` |
+
+The most common used case is to calculate the two time correlation function ``\left\langle \hat{A}(\tau) \hat{B}(0) \right\rangle``, which can be done by [`correlation_2op_1t`](@ref).
+
+```@setup correlation_and_spectrum
+using QuantumToolbox
+
+using CairoMakie
+CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
+```
+
+## Steadystate correlation function
+
+The following code demonstrates how to calculate the ``\langle \hat{x}(t) \hat{x}(0)\rangle`` correlation for a leaky cavity with three different relaxation rates ``\gamma``.
+
+```@example correlation_and_spectrum
+tlist = LinRange(0, 10, 200)
+a = destroy(10)
+x = a' + a
+H = a' * a
+
+# if the initial state is specified as `nothing`, the steady state will be calculated and used as the initial state.
+corr1 = correlation_2op_1t(H, nothing, tlist, [sqrt(0.5) * a], x, x)
+corr2 = correlation_2op_1t(H, nothing, tlist, [sqrt(1.0) * a], x, x)
+corr3 = correlation_2op_1t(H, nothing, tlist, [sqrt(2.0) * a], x, x)
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = L"Time $t$", ylabel = L"\langle \hat{x}(t) \hat{x}(0) \rangle")
+lines!(ax, tlist, real(corr1), label = L"\gamma = 0.5", linestyle = :solid)
+lines!(ax, tlist, real(corr2), label = L"\gamma = 1.0", linestyle = :dash)
+lines!(ax, tlist, real(corr3), label = L"\gamma = 2.0", linestyle = :dashdot)
+
+axislegend(ax, position = :rt)
+
+fig
+```
+
+## Emission spectrum
+
+Given a correlation function ``\left\langle \hat{A}(\tau) \hat{B}(0) \right\rangle``, we can define the corresponding power spectrum as
+
+```math
+S(\omega) = \int_{-\infty}^\infty \left\langle \hat{A}(\tau) \hat{B}(0) \right\rangle e^{-i \omega \tau} d \tau
+```
+
+In `QuantumToolbox.jl`, we can calculate ``S(\omega)`` using either [`spectrum`](@ref), which provides several solvers to perform the Fourier transform semi-analytically, or we can use the function [`spectrum_correlation_fft`](@ref) to numerically calculate the fast Fourier transform (FFT) of a given correlation data.
+
+The following example demonstrates how these methods can be used to obtain the emission (``\hat{A} = \hat{a}^\dagger`` and ``\hat{B} = \hat{a}``) power spectrum.
+
+```@example correlation_and_spectrum
+N = 4 # number of cavity fock states
+ωc = 1.0 * 2 * π # cavity frequency
+ωa = 1.0 * 2 * π # atom frequency
+g = 0.1 * 2 * π # coupling strength
+κ = 0.75 # cavity dissipation rate
+γ = 0.25 # atom dissipation rate
+
+# Jaynes-Cummings Hamiltonian
+a = tensor(destroy(N), qeye(2))
+sm = tensor(qeye(N), destroy(2))
+H = ωc * a' * a + ωa * sm' * sm + g * (a' * sm + a * sm')
+
+# collapse operators
+n_th = 0.25
+c_ops = [
+ sqrt(κ * (1 + n_th)) * a,
+ sqrt(κ * n_th) * a',
+ sqrt(γ) * sm,
+];
+
+# calculate the correlation function using mesolve, and then FFT to obtain the spectrum.
+# Here we need to make sure to evaluate the correlation function for a sufficient long time and
+# sufficiently high sampling rate so that FFT captures all the features in the resulting spectrum.
+tlist = LinRange(0, 100, 5000)
+corr = correlation_2op_1t(H, nothing, tlist, c_ops, a', a; progress_bar = Val(false))
+ωlist1, spec1 = spectrum_correlation_fft(tlist, corr)
+
+# calculate the power spectrum using spectrum
+# using Exponential Series (default) method
+ωlist2 = LinRange(0.25, 1.75, 200) * 2 * π
+spec2 = spectrum(H, ωlist2, c_ops, a', a; solver = ExponentialSeries())
+
+# calculate the power spectrum using spectrum
+# using Pseudo-Inverse method
+spec3 = spectrum(H, ωlist2, c_ops, a', a; solver = PseudoInverse())
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], title = "Vacuum Rabi splitting", xlabel = "Frequency", ylabel = "Emission power spectrum")
+lines!(ax, ωlist1 / (2 * π), spec1, label = "mesolve + FFT", linestyle = :solid)
+lines!(ax, ωlist2 / (2 * π), spec2, label = "Exponential Series", linestyle = :dash)
+lines!(ax, ωlist2 / (2 * π), spec3, label = "Pseudo-Inverse", linestyle = :dashdot)
+
+xlims!(ax, ωlist2[1] / (2 * π), ωlist2[end] / (2 * π))
+axislegend(ax, position = :rt)
+
+fig
+```
+
+## Non-steadystate correlation function
+
+More generally, we can also calculate correlation functions of the kind ``\left\langle \hat{A}(t_1 + t_2) \hat{B}(t_1) \right\rangle``, i.e., the correlation function of a system that is not in its steady state. In `QuantumToolbox.jl`, we can evaluate such correlation functions using the function [`correlation_2op_2t`](@ref). The default behavior of this function is to return a matrix with the correlations as a function of the two time coordinates (``t_1`` and ``t_2``).
+
+```@example correlation_and_spectrum
+t1_list = LinRange(0, 10.0, 200)
+t2_list = LinRange(0, 10.0, 200)
+
+N = 10
+a = destroy(N)
+x = a' + a
+H = a' * a
+
+c_ops = [sqrt(0.25) * a]
+
+α = 2.5
+ρ0 = coherent_dm(N, α)
+
+corr = correlation_2op_2t(H, ρ0, t1_list, t2_list, c_ops, x, x; progress_bar = Val(false))
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 400))
+
+ax = Axis(fig[1, 1], title = L"\langle \hat{x}(t_1 + t_2) \hat{x}(t_1) \rangle", xlabel = L"Time $t_1$", ylabel = L"Time $t_2$")
+
+heatmap!(ax, t1_list, t2_list, real(corr))
+
+fig
+```
+
+### Example: first-order optical coherence function
+
+This example demonstrates how to calculate a correlation function on the form ``\left\langle \hat{A}(\tau) \hat{B}(0) \right\rangle`` for a non-steady initial state. Consider an oscillator that is interacting with a thermal environment. If the oscillator initially is in a coherent state, it will gradually decay to a thermal (incoherent) state. The amount of coherence can be quantified using the first-order optical coherence function
+
+```math
+g^{(1)}(\tau) = \frac{\left\langle \hat{a}^\dagger(\tau) \hat{a}(0) \right\rangle}{\sqrt{\left\langle \hat{a}^\dagger(\tau) \hat{a}(\tau) \right\rangle \left\langle \hat{a}^\dagger(0) \hat{a}(0)\right\rangle}}.
+```
+For a coherent state ``\vert g^{(1)}(\tau) \vert = 1``, and for a completely incoherent (thermal) state ``g^{(1)}(\tau) = 0``. The following code calculates and plots ``g^{(1)}(\tau)`` as a function of ``\tau``:
+
+```@example correlation_and_spectrum
+τlist = LinRange(0, 10, 200)
+
+# Hamiltonian
+N = 15
+a = destroy(N)
+H = 2 * π * a' * a
+
+# collapse operator
+G1 = 0.75
+n_th = 2.00 # bath temperature in terms of excitation number
+c_ops = [
+ sqrt(G1 * (1 + n_th)) * a,
+ sqrt(G1 * n_th) * a'
+]
+
+# start with a coherent state of α = 2.0
+ρ0 = coherent_dm(N, 2.0)
+
+# first calculate the occupation number as a function of time
+n = mesolve(H, ρ0, τlist, c_ops, e_ops = [a' * a], progress_bar = Val(false)).expect[1,:]
+n0 = n[1] # occupation number at τ = 0
+
+# calculate the correlation function G1 and normalize with n to obtain g1
+g1 = correlation_2op_1t(H, ρ0, τlist, c_ops, a', a, progress_bar = Val(false))
+g1 = g1 ./ sqrt.(n .* n0)
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], title = "Decay of a coherent state to an incoherent (thermal) state", xlabel = L"Time $\tau$")
+lines!(ax, τlist, real(g1), label = L"g^{(1)}(\tau)", linestyle = :solid)
+lines!(ax, τlist, real(n), label = L"n(\tau)", linestyle = :dash)
+
+axislegend(ax, position = :rt)
+
+fig
+```
+
+### Example: second-order optical coherence function
+
+The second-order optical coherence function, with time-delay ``\tau``, is defined as
+
+```math
+g^{(2)}(\tau) = \frac{\left\langle \hat{a}^\dagger(0) \hat{a}^\dagger(\tau) \hat{a}(\tau) \hat{a}(0) \right\rangle}{\left\langle \hat{a}^\dagger(0) \hat{a}(0) \right\rangle^2}.
+```
+
+For a coherent state ``g^{(2)}(\tau) = 1``, for a thermal state ``g^{(2)}(\tau = 0) = 2`` and it decreases as a function of time (bunched photons, they tend to appear together), and for a Fock state with ``n``-photons ``g^{(2)}(\tau = 0) = n(n-1)/n^2 < 1`` and it increases with time (anti-bunched photons, more likely to arrive separated in time).
+
+To calculate this type of correlation function with `QuantumToolbox.jl`, we can use [`correlation_3op_1t`](@ref), which computes a correlation function on the form ``\left\langle \hat{A}(0) \hat{B}(\tau) \hat{C}(0) \right\rangle`` (three operators and one delay-time vector). We first have to combine the central two operators into one single one as they are evaluated at the same time, e.g. here we do ``\hat{B}(\tau) = \hat{a}^\dagger(\tau) \hat{a}(\tau) = (\hat{a}^\dagger\hat{a})(\tau)``.
+
+The following code calculates and plots ``g^{(2)}(\tau)`` as a function of ``\tau`` for a coherent, thermal and Fock state:
+
+```@example correlation_and_spectrum
+τlist = LinRange(0, 25, 200)
+
+# Hamiltonian
+N = 25
+a = destroy(N)
+H = 2 * π * a' * a
+
+κ = 0.25
+n_th = 2.0 # bath temperature in terms of excitation number
+c_ops = [
+ sqrt(κ * (1 + n_th)) * a,
+ sqrt(κ * n_th) * a'
+]
+
+cases = [
+ Dict("state" => coherent_dm(N, sqrt(2)), "label" => "coherent state", "lstyle" => :solid),
+ Dict("state" => thermal_dm(N, 2), "label" => "thermal state", "lstyle" => :dash),
+ Dict("state" => fock_dm(N, 2), "label" => "Fock state", "lstyle" => :dashdot),
+]
+
+# plot by CairoMakie.jl
+fig = Figure(size = (500, 350))
+ax = Axis(fig[1, 1], xlabel = L"Time $\tau$", ylabel = L"g^{(2)}(\tau)")
+
+for case in cases
+ ρ0 = case["state"]
+
+ # calculate the occupation number at τ = 0
+ n0 = expect(a' * a, ρ0)
+
+ # calculate the correlation function g2
+ g2 = correlation_3op_1t(H, ρ0, τlist, c_ops, a', a' * a, a, progress_bar = Val(false))
+ g2 = g2 ./ n0^2
+
+ lines!(ax, τlist, real(g2), label = case["label"], linestyle = case["lstyle"])
+end
+
+axislegend(ax, position = :rt)
+
+fig
+```
diff --git a/docs/src/users_guide/type_stability.md b/docs/src/users_guide/type_stability.md
deleted file mode 100644
index c3cd0e120..000000000
--- a/docs/src/users_guide/type_stability.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# [The Importance of Type-Stability](@id doc:Type-Stability)
-
-This page is still under construction.
diff --git a/ext/QuantumToolboxCUDAExt.jl b/ext/QuantumToolboxCUDAExt.jl
index e1b3418bb..e676b4ea4 100644
--- a/ext/QuantumToolboxCUDAExt.jl
+++ b/ext/QuantumToolboxCUDAExt.jl
@@ -1,67 +1,70 @@
module QuantumToolboxCUDAExt
using QuantumToolbox
-import CUDA: cu, CuArray
-import CUDA.CUSPARSE: CuSparseVector, CuSparseMatrixCSC, CuSparseMatrixCSR
-import SparseArrays: SparseVector, SparseMatrixCSC
+using QuantumToolbox: makeVal, getVal
+import QuantumToolbox: _sparse_similar, _convert_eltype_wordsize
+import CUDA: cu, CuArray, allowscalar
+import CUDA.CUSPARSE: CuSparseVector, CuSparseMatrixCSC, CuSparseMatrixCSR, AbstractCuSparseArray
+import SparseArrays: SparseVector, SparseMatrixCSC, sparse
+import CUDA.Adapt: adapt
+
+allowscalar(false)
@doc raw"""
CuArray(A::QuantumObject)
If `A.data` is a dense array, return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA.CuArray` for gpu calculations.
"""
-CuArray(A::QuantumObject{Tq}) where {Tq<:Union{Vector,Matrix}} = QuantumObject(CuArray(A.data), A.type, A.dims)
+CuArray(A::QuantumObject) = QuantumObject(CuArray(A.data), A.type, A.dimensions)
@doc raw"""
CuArray{T}(A::QuantumObject)
If `A.data` is a dense array, return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA.CuArray` with element type `T` for gpu calculations.
"""
-CuArray{T}(A::QuantumObject{Tq}) where {T,Tq<:Union{Vector,Matrix}} = QuantumObject(CuArray{T}(A.data), A.type, A.dims)
+CuArray{T}(A::QuantumObject) where {T} = QuantumObject(CuArray{T}(A.data), A.type, A.dimensions)
@doc raw"""
CuSparseVector(A::QuantumObject)
If `A.data` is a sparse vector, return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA.CUSPARSE.CuSparseVector` for gpu calculations.
"""
-CuSparseVector(A::QuantumObject{<:SparseVector}) = QuantumObject(CuSparseVector(A.data), A.type, A.dims)
+CuSparseVector(A::QuantumObject) = QuantumObject(CuSparseVector(A.data), A.type, A.dimensions)
@doc raw"""
CuSparseVector{T}(A::QuantumObject)
If `A.data` is a sparse vector, return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA.CUSPARSE.CuSparseVector` with element type `T` for gpu calculations.
"""
-CuSparseVector{T}(A::QuantumObject{<:SparseVector}) where {T} = QuantumObject(CuSparseVector{T}(A.data), A.type, A.dims)
+CuSparseVector{T}(A::QuantumObject) where {T} = QuantumObject(CuSparseVector{T}(A.data), A.type, A.dimensions)
@doc raw"""
CuSparseMatrixCSC(A::QuantumObject)
If `A.data` is in the type of `SparseMatrixCSC`, return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA.CUSPARSE.CuSparseMatrixCSC` for gpu calculations.
"""
-CuSparseMatrixCSC(A::QuantumObject{<:SparseMatrixCSC}) = QuantumObject(CuSparseMatrixCSC(A.data), A.type, A.dims)
+CuSparseMatrixCSC(A::QuantumObject) = QuantumObject(CuSparseMatrixCSC(A.data), A.type, A.dimensions)
@doc raw"""
CuSparseMatrixCSC{T}(A::QuantumObject)
If `A.data` is in the type of `SparseMatrixCSC`, return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA.CUSPARSE.CuSparseMatrixCSC` with element type `T` for gpu calculations.
"""
-CuSparseMatrixCSC{T}(A::QuantumObject{<:SparseMatrixCSC}) where {T} =
- QuantumObject(CuSparseMatrixCSC{T}(A.data), A.type, A.dims)
+CuSparseMatrixCSC{T}(A::QuantumObject) where {T} = QuantumObject(CuSparseMatrixCSC{T}(A.data), A.type, A.dimensions)
@doc raw"""
CuSparseMatrixCSR(A::QuantumObject)
If `A.data` is in the type of `SparseMatrixCSC`, return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA.CUSPARSE.CuSparseMatrixCSR` for gpu calculations.
"""
-CuSparseMatrixCSR(A::QuantumObject{<:SparseMatrixCSC}) = QuantumObject(CuSparseMatrixCSR(A.data), A.type, A.dims)
+CuSparseMatrixCSR(A::QuantumObject) = QuantumObject(CuSparseMatrixCSR(A.data), A.type, A.dimensions)
@doc raw"""
CuSparseMatrixCSR(A::QuantumObject)
If `A.data` is in the type of `SparseMatrixCSC`, return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA.CUSPARSE.CuSparseMatrixCSR` with element type `T` for gpu calculations.
"""
-CuSparseMatrixCSR{T}(A::QuantumObject{<:SparseMatrixCSC}) where {T} =
- QuantumObject(CuSparseMatrixCSR{T}(A.data), A.type, A.dims)
+CuSparseMatrixCSR{T}(A::QuantumObject) where {T} = QuantumObject(CuSparseMatrixCSR{T}(A.data), A.type, A.dimensions)
@doc raw"""
cu(A::QuantumObject; word_size::Int=64)
@@ -72,21 +75,34 @@ Return a new [`QuantumObject`](@ref) where `A.data` is in the type of `CUDA` arr
- `A::QuantumObject`: The [`QuantumObject`](@ref)
- `word_size::Int`: The word size of the element type of `A`, can be either `32` or `64`. Default to `64`.
"""
-cu(A::QuantumObject; word_size::Int = 64) =
- ((word_size == 64) || (word_size == 32)) ? cu(A, Val(word_size)) :
- throw(DomainError(word_size, "The word size should be 32 or 64."))
-cu(A::QuantumObject{T}, word_size::TW) where {T<:Union{Vector,Matrix},TW<:Union{Val{32},Val{64}}} =
- CuArray{_change_eltype(eltype(A), word_size)}(A)
-cu(A::QuantumObject{<:SparseVector}, word_size::TW) where {TW<:Union{Val{32},Val{64}}} =
- CuSparseVector{_change_eltype(eltype(A), word_size)}(A)
-cu(A::QuantumObject{<:SparseMatrixCSC}, word_size::TW) where {TW<:Union{Val{32},Val{64}}} =
- CuSparseMatrixCSC{_change_eltype(eltype(A), word_size)}(A)
-
-_change_eltype(::Type{T}, ::Val{64}) where {T<:Int} = Int64
-_change_eltype(::Type{T}, ::Val{32}) where {T<:Int} = Int32
-_change_eltype(::Type{T}, ::Val{64}) where {T<:AbstractFloat} = Float64
-_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
+function cu(A::QuantumObject; word_size::Union{Val,Int} = Val(64))
+ _word_size = getVal(makeVal(word_size))
+
+ ((_word_size == 64) || (_word_size == 32)) || throw(DomainError(_word_size, "The word size should be 32 or 64."))
+
+ return cu(A, makeVal(word_size))
+end
+cu(A::QuantumObject, word_size::Union{Val{32},Val{64}}) =
+ QuantumObject(adapt(CuArray{_convert_eltype_wordsize(eltype(A), word_size)}, A.data), A.type, A.dimensions)
+function cu(
+ A::QuantumObject{ObjType,DimsType,<:SparseVector},
+ word_size::Union{Val{32},Val{64}},
+) where {ObjType<:QuantumObjectType,DimsType<:AbstractDimensions}
+ return CuSparseVector{_convert_eltype_wordsize(eltype(A), word_size)}(A)
+end
+function cu(
+ A::QuantumObject{ObjType,DimsType,<:SparseMatrixCSC},
+ word_size::Union{Val{32},Val{64}},
+) where {ObjType<:QuantumObjectType,DimsType<:AbstractDimensions}
+ return CuSparseMatrixCSC{_convert_eltype_wordsize(eltype(A), word_size)}(A)
+end
+
+QuantumToolbox.to_dense(A::MT) where {MT<:AbstractCuSparseArray} = CuArray(A)
+
+QuantumToolbox.to_dense(::Type{T1}, A::CuArray{T2}) where {T1<:Number,T2<:Number} = CuArray{T1}(A)
+QuantumToolbox.to_dense(::Type{T}, A::AbstractCuSparseArray) where {T<:Number} = CuArray{T}(A)
+
+QuantumToolbox._sparse_similar(A::CuSparseMatrixCSC, args...) = sparse(args..., fmt = :csc)
+QuantumToolbox._sparse_similar(A::CuSparseMatrixCSR, args...) = sparse(args..., fmt = :csr)
end
diff --git a/ext/QuantumToolboxChainRulesCoreExt.jl b/ext/QuantumToolboxChainRulesCoreExt.jl
new file mode 100644
index 000000000..968d2e674
--- /dev/null
+++ b/ext/QuantumToolboxChainRulesCoreExt.jl
@@ -0,0 +1,14 @@
+module QuantumToolboxChainRulesCoreExt
+
+using LinearAlgebra
+import QuantumToolbox: QuantumObject
+import ChainRulesCore: rrule, NoTangent, Tangent
+
+function rrule(::Type{QuantumObject}, data, type, dimensions)
+ obj = QuantumObject(data, type, dimensions)
+ f_pullback(Δobj) = (NoTangent(), Δobj.data, NoTangent(), NoTangent())
+ f_pullback(Δobj_data::AbstractArray) = (NoTangent(), Δobj_data, NoTangent(), NoTangent())
+ return obj, f_pullback
+end
+
+end
diff --git a/ext/QuantumToolboxGPUArraysExt.jl b/ext/QuantumToolboxGPUArraysExt.jl
new file mode 100644
index 000000000..d9b686819
--- /dev/null
+++ b/ext/QuantumToolboxGPUArraysExt.jl
@@ -0,0 +1,33 @@
+module QuantumToolboxGPUArraysExt
+
+using QuantumToolbox
+
+import GPUArrays: AbstractGPUArray
+import KernelAbstractions
+import KernelAbstractions: @kernel, @Const, @index, get_backend, synchronize
+
+@kernel function tr_kernel!(B, @Const(A))
+ # i, j, k = @index(Global, NTuple)
+ # Atomix.@atomic B[i, j] += A[i, j, k, k] # TODO: use Atomix when it will support Complex types
+
+ i, j = @index(Global, NTuple)
+ @inbounds B[i, j] = 0
+ @inbounds for k in 1:size(A, 3)
+ B[i, j] += A[i, j, k, k]
+ end
+end
+
+function QuantumToolbox._map_trace(A::AbstractGPUArray{T,4}) where {T}
+ B = similar(A, size(A, 1), size(A, 2))
+ fill!(B, 0)
+
+ backend = get_backend(A)
+ kernel! = tr_kernel!(backend)
+
+ kernel!(B, A, ndrange = size(A)[1:2])
+ KernelAbstractions.synchronize(backend)
+
+ return B
+end
+
+end
diff --git a/ext/QuantumToolboxMakieExt.jl b/ext/QuantumToolboxMakieExt.jl
new file mode 100644
index 000000000..9f0254185
--- /dev/null
+++ b/ext/QuantumToolboxMakieExt.jl
@@ -0,0 +1,719 @@
+module QuantumToolboxMakieExt
+
+using QuantumToolbox
+import QuantumToolbox: _state_to_bloch
+
+import LinearAlgebra: cross, deg2rad, normalize, size
+import Makie:
+ Axis,
+ Axis3,
+ LScene,
+ Colorbar,
+ Figure,
+ GridLayout,
+ heatmap!,
+ surface!,
+ barplot!,
+ GridPosition,
+ @L_str,
+ Reverse,
+ ylims!,
+ RGBAf,
+ Sphere,
+ lines!,
+ scatter!,
+ arrows3d!,
+ text!,
+ mesh!,
+ RGBf,
+ Point3f,
+ NoShading,
+ cameracontrols,
+ update_cam!,
+ cam3d!
+
+@doc raw"""
+ plot_wigner(
+ library::Val{:Makie},
+ state::QuantumObject{OpType};
+ xvec::Union{Nothing,AbstractVector} = nothing,
+ yvec::Union{Nothing,AbstractVector} = nothing,
+ g::Real = √2,
+ method::WignerSolver = WignerClenshaw(),
+ projection::Union{Val,Symbol} = Val(:two_dim),
+ location::Union{GridPosition,Nothing} = nothing,
+ colorbar::Bool = false,
+ kwargs...
+ ) where {OpType}
+
+Plot the [Wigner quasipropability distribution](https://en.wikipedia.org/wiki/Wigner_quasiprobability_distribution) of `state` using the [`Makie`](https://github.com/MakieOrg/Makie.jl) plotting library.
+
+# Arguments
+- `library::Val{:Makie}`: The plotting library to use.
+- `state::QuantumObject`: The quantum state for which the Wigner function is calculated. It can be either a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+- `xvec::AbstractVector`: The x-coordinates of the phase space grid. Defaults to a linear range from -7.5 to 7.5 with 200 points.
+- `yvec::AbstractVector`: The y-coordinates of the phase space grid. Defaults to a linear range from -7.5 to 7.5 with 200 points.
+- `g::Real`: The scaling factor related to the value of ``\hbar`` in the commutation relation ``[x, y] = i \hbar`` via ``\hbar=2/g^2``.
+- `method::WignerSolver`: The method used to calculate the Wigner function. It can be either `WignerLaguerre()` or `WignerClenshaw()`, with `WignerClenshaw()` as default. The `WignerLaguerre` method has the optional `parallel` and `tol` parameters, with default values `true` and `1e-14`, respectively.
+- `projection::Union{Val,Symbol}`: Whether to plot the Wigner function in 2D or 3D. It can be either `Val(:two_dim)` or `Val(:three_dim)`, with `Val(:two_dim)` as default.
+- `location::Union{GridPosition,Nothing}`: The location of the plot in the layout. If `nothing`, the plot is created in a new figure. Default is `nothing`.
+- `colorbar::Bool`: Whether to include a colorbar in the plot. Default is `false`.
+- `kwargs...`: Additional keyword arguments to pass to the plotting function.
+
+# Returns
+- `fig`: The figure object.
+- `ax`: The axis object.
+- `hm`: Either the heatmap or surface object, depending on the projection.
+
+!!! note "Import library first"
+ [`Makie.jl`](https://github.com/MakieOrg/Makie.jl) must first be imported before using this function. This can be done by importing one of the available backends, such as [`CairoMakie.jl`](https://github.com/MakieOrg/Makie.jl/tree/master/CairoMakie), [`GLMakie.jl`](https://github.com/MakieOrg/Makie.jl/tree/master/GLMakie), or [`WGLMakie.jl`](https://github.com/MakieOrg/Makie.jl/tree/master/WGLMakie).
+
+!!! warning "Beware of type-stability!"
+ If you want to keep type stability, it is recommended to use `Val(:two_dim)` and `Val(:three_dim)` instead of `:two_dim` and `:three_dim`, respectively. Also, specify the library as `Val(:Makie)` 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 QuantumToolbox.plot_wigner(
+ library::Val{:Makie},
+ state::QuantumObject{OpType};
+ xvec::Union{Nothing,AbstractVector} = LinRange(-7.5, 7.5, 200),
+ yvec::Union{Nothing,AbstractVector} = LinRange(-7.5, 7.5, 200),
+ g::Real = √2,
+ method::WignerSolver = WignerClenshaw(),
+ projection::Union{Val,Symbol} = Val(:two_dim),
+ location::Union{GridPosition,Nothing} = nothing,
+ colorbar::Bool = false,
+ kwargs...,
+) where {OpType<:Union{Bra,Ket,Operator}}
+ QuantumToolbox.getVal(projection) == :two_dim ||
+ QuantumToolbox.getVal(projection) == :three_dim ||
+ throw(ArgumentError("Unsupported projection: $projection"))
+
+ return _plot_wigner(
+ library,
+ state,
+ xvec,
+ yvec,
+ QuantumToolbox.makeVal(projection),
+ g,
+ method,
+ location,
+ colorbar;
+ kwargs...,
+ )
+end
+
+function _plot_wigner(
+ ::Val{:Makie},
+ state::QuantumObject{OpType},
+ xvec::AbstractVector,
+ yvec::AbstractVector,
+ projection::Val{:two_dim},
+ g::Real,
+ method::WignerSolver,
+ location::Union{GridPosition,Nothing},
+ colorbar::Bool;
+ kwargs...,
+) where {OpType<:Union{Bra,Ket,Operator}}
+ fig, location = _getFigAndLocation(location)
+
+ lyt = GridLayout(location)
+
+ ax = Axis(lyt[1, 1])
+
+ wig = wigner(state, xvec, yvec; g = g, method = method)
+ wlim = maximum(abs, wig)
+
+ kwargs = merge(Dict(:colormap => Reverse(:RdBu), :colorrange => (-wlim, wlim)), kwargs)
+ hm = heatmap!(ax, xvec, yvec, transpose(wig); kwargs...)
+
+ if colorbar
+ Colorbar(lyt[1, 2], hm)
+ end
+
+ ax.xlabel = L"\textrm{Re}(\alpha)"
+ ax.ylabel = L"\textrm{Im}(\alpha)"
+ return fig, ax, hm
+end
+
+function _plot_wigner(
+ ::Val{:Makie},
+ state::QuantumObject{OpType},
+ xvec::AbstractVector,
+ yvec::AbstractVector,
+ projection::Val{:three_dim},
+ g::Real,
+ method::WignerSolver,
+ location::Union{GridPosition,Nothing},
+ colorbar::Bool;
+ kwargs...,
+) where {OpType<:Union{Bra,Ket,Operator}}
+ fig, location = _getFigAndLocation(location)
+
+ lyt = GridLayout(location)
+
+ ax = Axis3(lyt[1, 1], azimuth = 1.775pi, elevation = pi / 16, protrusions = (30, 90, 30, 30), viewmode = :stretch)
+
+ wig = wigner(state, xvec, yvec; g = g, method = method)
+ wlim = maximum(abs, wig)
+
+ kwargs = merge(Dict(:colormap => :RdBu, :colorrange => (-wlim, wlim)), kwargs)
+ surf = surface!(ax, xvec, yvec, transpose(wig); kwargs...)
+
+ if colorbar
+ Colorbar(lyt[1, 2], surf)
+ end
+
+ ax.xlabel = L"\textrm{Re}(\alpha)"
+ ax.ylabel = L"\textrm{Im}(\alpha)"
+ ax.zlabel = "Wigner function"
+ return fig, ax, surf
+end
+
+@doc raw"""
+ plot_fock_distribution(
+ library::Val{:Makie},
+ ρ::QuantumObject{SType};
+ fock_numbers::Union{Nothing, AbstractVector} = nothing,
+ unit_y_range::Bool = true,
+ location::Union{GridPosition,Nothing} = nothing,
+ kwargs...
+ ) where {SType<:Union{Ket,Operator}}
+
+Plot the [Fock state](https://en.wikipedia.org/wiki/Fock_state) distribution of `ρ`.
+
+# Arguments
+- `library::Val{:Makie}`: The plotting library to use.
+- `ρ::QuantumObject`: The quantum state for which the Fock state distribution is to be plotted. It can be either a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+- `location::Union{GridPosition,Nothing}`: The location of the plot in the layout. If `nothing`, the plot is created in a new figure. Default is `nothing`.
+- `fock_numbers::Union{Nothing, AbstractVector}`: list of x ticklabels to represent fock numbers, default is `nothing`.
+- `unit_y_range::Bool`: Set y-axis limits [0, 1] or not, default is `true`.
+- `kwargs...`: Additional keyword arguments to pass to the plotting function.
+
+# Returns
+- `fig`: The figure object.
+- `ax`: The axis object.
+- `bp`: The barplot object.
+
+!!! note "Import library first"
+ [`Makie.jl`](https://github.com/MakieOrg/Makie.jl) must first be imported before using this function. This can be done by importing one of the available backends, such as [`CairoMakie.jl`](https://github.com/MakieOrg/Makie.jl/tree/master/CairoMakie), [`GLMakie.jl`](https://github.com/MakieOrg/Makie.jl/tree/master/GLMakie), or [`WGLMakie.jl`](https://github.com/MakieOrg/Makie.jl/tree/master/WGLMakie).
+
+!!! warning "Beware of type-stability!"
+ If you want to keep type stability, it is recommended to use `Val(:two_dim)` and `Val(:three_dim)` instead of `:two_dim` and `:three_dim`, respectively. Also, specify the library as `Val(:Makie)` 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 QuantumToolbox.plot_fock_distribution(
+ library::Val{:Makie},
+ ρ::QuantumObject{SType};
+ fock_numbers::Union{Nothing,AbstractVector} = nothing,
+ unit_y_range::Bool = true,
+ location::Union{GridPosition,Nothing} = nothing,
+ kwargs...,
+) where {SType<:Union{Bra,Ket,Operator}}
+ return _plot_fock_distribution(
+ library,
+ ρ;
+ fock_numbers = fock_numbers,
+ unit_y_range = unit_y_range,
+ location = location,
+ kwargs...,
+ )
+end
+
+function _plot_fock_distribution(
+ ::Val{:Makie},
+ ρ::QuantumObject{SType};
+ fock_numbers::Union{Nothing,AbstractVector} = nothing,
+ unit_y_range::Bool = true,
+ location::Union{GridPosition,Nothing} = nothing,
+ kwargs...,
+) where {SType<:Union{Bra,Ket,Operator}}
+ ρ = ket2dm(ρ)
+ D = prod(ρ.dims)
+ isapprox(tr(ρ), 1, atol = 1e-4) || (@warn "The input ρ should be normalized.")
+
+ xvec = 0:(D-1)
+ isnothing(fock_numbers) && (fock_numbers = string.(collect(xvec)))
+
+ fig, location = _getFigAndLocation(location)
+ lyt = GridLayout(location)
+ ax = Axis(lyt[1, 1])
+
+ bp = barplot!(ax, xvec, real(diag(ρ)); kwargs...)
+
+ ax.xticks = (xvec, fock_numbers)
+ ax.xlabel = "Fock number"
+ ax.ylabel = "Occupation probability"
+ unit_y_range && ylims!(ax, 0, 1)
+
+ return fig, ax, bp
+end
+
+raw"""
+ _getFigAndLocation(location::Nothing)
+
+ Create a new figure and return it, together with the GridPosition object pointing to the first cell.
+
+ # Arguments
+ - `location::Nothing`
+
+ # Returns
+ - `fig`: The figure object.
+ - `location`: The GridPosition object pointing to the first cell.
+"""
+function _getFigAndLocation(location::Nothing)
+ fig = Figure()
+ return fig, fig[1, 1]
+end
+
+raw"""
+ _getFigAndLocation(location::GridPosition)
+
+ Compute which figure does the location belong to and return it, together with the location itself.
+
+ # Arguments
+ - `location::GridPosition`
+
+ # Returns
+ - `fig`: The figure object.
+ - `location`: The GridPosition object.
+"""
+function _getFigAndLocation(location::GridPosition)
+ fig = _figFromChildren(location.layout)
+ return fig, location
+end
+
+raw"""
+ _figFromChildren(children::GridLayout)
+
+ Recursively find the figure object from the children layout.
+
+ # Arguments
+ - `children::GridLayout`
+
+ # Returns
+ - Union{Nothing, Figure, GridLayout}: The children's parent object.
+"""
+_figFromChildren(children) = _figFromChildren(children.parent)
+
+raw"""
+ _figFromChildren(fig::Figure)
+
+ Return the figure object
+
+ # Arguments
+ - `fig::Figure`
+
+ # Returns
+ - `fig`: The figure object.
+"""
+_figFromChildren(fig::Figure) = fig
+
+raw"""
+ _figFromChildren(::Nothing)
+
+ Throw an error if no figure has been found.
+
+ # Arguments
+ - `::Nothing`
+
+ # Throws
+ - `ArgumentError`: If no figure has been found.
+"""
+_figFromChildren(::Nothing) = throw(ArgumentError("No Figure has been found at the top of the layout hierarchy."))
+
+function _render_bloch_makie(bloch_vec::Vector{Float64}; location = nothing, kwargs...)
+ b = Bloch()
+ add_vectors!(b, bloch_vec)
+ fig, location = _getFigAndLocation(location)
+ fig, ax = render(b; location = location, kwargs...)
+ return fig, ax
+end
+
+@doc raw"""
+ render(b::Bloch; location=nothing)
+
+Render the Bloch sphere visualization from the given [`Bloch`](@ref) object `b`.
+
+# Arguments
+
+- `b::Bloch`: The Bloch sphere object containing states, vectors, and settings to visualize.
+- `location::Union{GridPosition,LScene,Nothing}`: The location of the plot in the layout, or `Makie.LScene`. Default is `nothing`.
+
+
+# Returns
+
+- A tuple `(fig, lscene)` where `fig` is the figure object and `lscene` is the LScene object used for plotting. These can be further manipulated or saved by the user.
+
+# Notes
+
+The keyword argument `location` can be in the either type:
+
+- `Nothing` (default): Create a new figure and plot the Bloch sphere.
+- `GridPosition`: Plot the Bloch sphere in the specified location of the plot in the layout.
+- `LScene`: Update the existing Bloch sphere using new data and settings in `b::Bloch` without creating new `Figure` and `LScene` (efficient for drawing animation).
+"""
+function QuantumToolbox.render(b::Bloch; location = nothing)
+ fig, lscene = _setup_bloch_plot!(location)
+ _setup_bloch_camara!(b, lscene)
+ _draw_bloch_sphere!(b, lscene)
+ _add_labels!(b, lscene)
+
+ # plot data fields in Bloch
+ _plot_vectors!(b, lscene)
+ _plot_lines!(b, lscene)
+ _plot_arcs!(b, lscene)
+ _plot_points!(b, lscene) # plot points at the end so that they will be on the very top (front) figure layer.
+
+ return fig, lscene
+end
+
+raw"""
+ _setup_bloch_plot!(location) -> (fig, lscene)
+
+Initialize the Figure and LScene for Bloch sphere visualization.
+
+# Arguments
+- `location`: Figure layout position specification, or directly `Makie.LScene` for updating Bloch sphere.
+
+# Returns
+- `fig`: Created Makie figure
+- `lscene`: Configured LScene object
+"""
+function _setup_bloch_plot!(location)
+ fig, location = _getFigAndLocation(location)
+ lscene = LScene(location, show_axis = false, scenekw = (clear = true,))
+ return fig, lscene
+end
+
+function _setup_bloch_plot!(lscene::LScene)
+ # this function only removes all existing Plots in lscene
+ # it is useful for users to just update Bloch sphere without creating new figure and lscene (efficient for drawing animation)
+ fig = lscene.parent
+ empty!(lscene.scene.plots)
+ return fig, lscene
+end
+
+raw"""
+ _setup_bloch_camara!(b::Bloch, lscene)
+
+Setup the distance and view angle of the camara.
+"""
+function _setup_bloch_camara!(b::Bloch, lscene)
+ length(b.view) == 2 || throw(ArgumentError("The length of `Bloch.view` must be 2."))
+ cam3d!(lscene.scene, center = false)
+ cam = cameracontrols(lscene)
+ cam.fov[] = 12 # Set field of view to 12 degrees
+ dist = 12 # Set distance from the camera to the Bloch sphere
+ update_cam!(lscene.scene, cam, deg2rad(b.view[1]), deg2rad(b.view[2]), dist)
+ return nothing
+end
+
+raw"""
+ _draw_bloch_sphere!(b::Bloch, lscene)
+
+Draw the translucent sphere, axes, and reference circles representing the Bloch sphere surface.
+"""
+function _draw_bloch_sphere!(b::Bloch, lscene)
+ radius = 1.0f0
+ sphere_mesh = Sphere(Point3f(0), radius)
+ mesh!(
+ lscene,
+ sphere_mesh;
+ color = b.sphere_color,
+ alpha = b.sphere_alpha,
+ shading = NoShading,
+ transparency = true,
+ rasterize = 3,
+ )
+
+ # X, Y, and Z axes
+ axes = [
+ [Point3f(1.0, 0, 0), Point3f(-1.0, 0, 0)], # X-axis
+ [Point3f(0, 1.0, 0), Point3f(0, -1.0, 0)], # Y-axis
+ [Point3f(0, 0, 1.0), Point3f(0, 0, -1.0)], # Z-axis
+ ]
+ for points in axes
+ lines!(lscene, points; color = b.frame_color)
+ end
+
+ # highlight circles for XY and XZ planes
+ φ = range(0, 2π, length = 100)
+ lines!(lscene, [Point3f(cos(φi), sin(φi), 0) for φi in φ]; color = b.frame_color, linewidth = b.frame_width) # XY
+ lines!(lscene, [Point3f(cos(φi), 0, sin(φi)) for φi in φ]; color = b.frame_color, linewidth = b.frame_width) # XZ
+
+ # other curves of longitude (with polar angle φ and azimuthal angle θ)
+ φ_curve = range(0, 2π, 600)
+ θ_vals = [1, 2, 3] * π / 4
+ for θi in θ_vals
+ x_line = radius * sin.(φ_curve) .* cos(θi)
+ y_line = radius * sin.(φ_curve) .* sin(θi)
+ z_line = radius * cos.(φ_curve)
+ lines!(lscene, x_line, y_line, z_line; color = b.frame_color, alpha = b.frame_alpha, linewidth = b.frame_width)
+ end
+
+ # other curves of latitude (with polar angle φ and azimuthal angle θ)
+ φ_vals = [1, 3] * π / 4 # missing `2` because XY plane has already be handled above
+ θ_curve = range(0, 2π, 600)
+ for ϕ in φ_vals
+ x_ring = radius * sin(ϕ) .* cos.(θ_curve)
+ y_ring = radius * sin(ϕ) .* sin.(θ_curve)
+ z_ring = fill(radius * cos(ϕ), length(θ_curve))
+ lines!(lscene, x_ring, y_ring, z_ring; color = b.frame_color, alpha = b.frame_alpha, linewidth = b.frame_width)
+ end
+ return nothing
+end
+
+raw"""
+ _add_labels!(b::Bloch, lscene)
+
+Add axis labels and state labels to the Bloch sphere.
+
+# Arguments
+- `lscene`: LScene object for text placement
+
+Positions standard labels `(x, y, |0⟩, |1⟩)` at appropriate locations.
+"""
+function _add_labels!(b::Bloch, lscene)
+ length(b.xlabel) == 2 || throw(ArgumentError("The length of `Bloch.xlabel` must be 2."))
+ length(b.ylabel) == 2 || throw(ArgumentError("The length of `Bloch.ylabel` must be 2."))
+ length(b.zlabel) == 2 || throw(ArgumentError("The length of `Bloch.zlabel` must be 2."))
+ length(b.xlpos) == 2 || throw(ArgumentError("The length of `Bloch.xlpos` must be 2."))
+ length(b.ylpos) == 2 || throw(ArgumentError("The length of `Bloch.ylpos` must be 2."))
+ length(b.zlpos) == 2 || throw(ArgumentError("The length of `Bloch.zlpos` must be 2."))
+
+ label_color = parse(RGBf, b.font_color)
+ label_size = b.font_size
+
+ (b.xlabel[1] == "") || text!(
+ lscene,
+ b.xlabel[1],
+ position = Point3f(b.xlpos[1], 0, 0),
+ color = label_color,
+ fontsize = label_size,
+ align = (:center, :center),
+ )
+ (b.xlabel[2] == "") || text!(
+ lscene,
+ b.xlabel[2],
+ position = Point3f(b.xlpos[2], 0, 0),
+ color = label_color,
+ fontsize = label_size,
+ align = (:center, :center),
+ )
+ (b.ylabel[1] == "") || text!(
+ lscene,
+ b.ylabel[1],
+ position = Point3f(0, b.ylpos[1], 0),
+ color = label_color,
+ fontsize = label_size,
+ align = (:center, :center),
+ )
+ (b.ylabel[2] == "") || text!(
+ lscene,
+ b.ylabel[2],
+ position = Point3f(0, b.ylpos[2], 0),
+ color = label_color,
+ fontsize = label_size,
+ align = (:center, :center),
+ )
+ (b.zlabel[1] == "") || text!(
+ lscene,
+ b.zlabel[1],
+ position = Point3f(0, 0, b.zlpos[1]),
+ color = label_color,
+ fontsize = label_size,
+ align = (:center, :center),
+ )
+ (b.zlabel[2] == "") || text!(
+ lscene,
+ b.zlabel[2],
+ position = Point3f(0, 0, b.zlpos[2]),
+ color = label_color,
+ fontsize = label_size,
+ align = (:center, :center),
+ )
+ return nothing
+end
+
+raw"""
+ _plot_points!(b::Bloch, lscene)
+
+Plot all quantum state points on the Bloch sphere.
+
+# Arguments
+- `b::Bloch`: Contains point data and styling information
+- `lscene`: LScene object for plotting
+
+Handles both scatter points and line traces based on style specifications.
+"""
+function _plot_points!(b::Bloch, lscene)
+ isempty(b.points) && return nothing
+ for k in 1:length(b.points)
+ pts = b.points[k]
+ style = b.point_style[k]
+ alpha = b.point_alpha[k]
+ marker = b.point_marker[mod1(k, length(b.point_marker))]
+ N = size(pts, 2)
+
+ raw_x = pts[1, :]
+ raw_y = pts[2, :]
+ raw_z = pts[3, :]
+
+ ds = vec(sqrt.(sum(abs2, pts; dims = 1)))
+ if !all(isapprox.(ds, ds[1]; rtol = 1e-12))
+ indperm = sortperm(ds)
+ else
+ indperm = collect(1:N)
+ end
+ this_color = b.point_color[k]
+ if style == :m
+ defaults = b.point_default_color
+ L = length(defaults)
+ times = ceil(Int, N / L)
+ big_colors = repeat(b.point_default_color, times)[1:N]
+ big_colors = big_colors[indperm]
+ colors = big_colors
+ else
+ if this_color === nothing
+ defaults = b.point_default_color
+ colors = defaults[mod1(k, length(defaults))]
+ else
+ colors = this_color
+ end
+ end
+ if style in (:s, :m)
+ scatter!(
+ lscene,
+ raw_x[indperm],
+ raw_y[indperm],
+ raw_z[indperm];
+ color = colors,
+ markersize = b.point_size[mod1(k, length(b.point_size))],
+ marker = marker,
+ transparency = alpha < 1.0,
+ alpha = alpha,
+ strokewidth = 0.0,
+ )
+
+ elseif style == :l
+ c = isa(colors, Vector) ? colors[1] : colors
+ lines!(lscene, raw_x, raw_y, raw_z; color = c, transparency = alpha < 1.0, alpha = alpha)
+ end
+ end
+ return nothing
+end
+
+raw"""
+ _plot_lines!(b::Bloch, lscene)
+
+Draw all connecting lines between points on the Bloch sphere.
+
+# Arguments
+- `b::Bloch`: Contains line data and formatting
+- `lscene`: LScene object for drawing
+
+Processes line style specifications and color mappings.
+"""
+function _plot_lines!(b::Bloch, lscene)
+ isempty(b.lines) && return nothing
+ color_map =
+ Dict("k" => :black, "r" => :red, "g" => :green, "b" => :blue, "c" => :cyan, "m" => :magenta, "y" => :yellow)
+ for (line, fmt) in b.lines
+ x, y, z = line
+ color_char = first(fmt)
+ color = get(color_map, color_char, :black)
+ linestyle = if occursin("--", fmt)
+ :dash
+ elseif occursin(":", fmt)
+ :dot
+ elseif occursin("-.", fmt)
+ :dashdot
+ else
+ :solid
+ end
+ lines!(lscene, x, y, z; color = color, linestyle = linestyle)
+ end
+ return nothing
+end
+
+raw"""
+ _plot_arcs!(b::Bloch, lscene)
+
+Draw circular arcs connecting points on the Bloch sphere surface.
+
+# Arguments
+- `b::Bloch`: Contains arc data points
+- `lscene`: LScene object for drawing
+
+Calculates great circle arcs between specified points.
+"""
+function _plot_arcs!(b::Bloch, lscene)
+ isempty(b.arcs) && return nothing
+ for arc_pts in b.arcs
+ length(arc_pts) >= 2 || continue
+ v1 = normalize(arc_pts[1])
+ v2 = normalize(arc_pts[end])
+ n = normalize(cross(v1, v2))
+ θ = acos(clamp(dot(v1, v2), -1.0, 1.0))
+ if length(arc_pts) == 3
+ vm = normalize(arc_pts[2])
+ dot(cross(v1, vm), n) < 0 && (θ -= 2π)
+ end
+ t_range = range(0, θ, length = 100)
+ arc_points = [Point3f(v1 * cos(t) + cross(n, v1) * sin(t)) for t in t_range]
+ lines!(lscene, arc_points; color = "blue", linestyle = :solid)
+ end
+ return nothing
+end
+
+raw"""
+ _plot_vectors!(b::Bloch, lscene)
+
+Draw vectors from origin representing quantum states.
+
+# Arguments
+- `b::Bloch`: Contains vector data
+- `lscene`: LScene object for drawing
+
+Scales vectors appropriately and adds `3D` arrow markers.
+"""
+function _plot_vectors!(b::Bloch, lscene)
+ isempty(b.vectors) && return nothing
+
+ for (i, v) in enumerate(b.vectors)
+ color = get(b.vector_color, i, RGBAf(0.2, 0.5, 0.8, 0.9))
+
+ arrows3d!(
+ lscene,
+ Point3f(0),
+ Point3f(v),
+ color = color,
+ shaftradius = b.vector_width,
+ tiplength = b.vector_tiplength,
+ tipradius = b.vector_tipradius,
+ # rasterize = 3, #TODO: maybe uncomment this after https://github.com/MakieOrg/Makie.jl/issues/5259 is fixed
+ )
+ end
+ return nothing
+end
+
+@doc raw"""
+ plot_bloch(::Val{:Makie}, state::QuantumObject; kwargs...)
+
+Plot a pure quantum state on the Bloch sphere using the `Makie` backend.
+
+# Arguments
+- `state::QuantumObject{<:Union{Ket,Bra}}`: The quantum state ([`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref)) to be visualized.
+- `kwargs...`: Additional keyword arguments passed to `_render_bloch_makie`.
+
+!!! note "Internal function"
+ This is the `Makie`-specific implementation called by the main `plot_bloch` function.
+"""
+function QuantumToolbox.plot_bloch(
+ ::Val{:Makie},
+ state::QuantumObject{OpType};
+ kwargs...,
+) where {OpType<:Union{Ket,Bra,Operator}}
+ bloch_vec = _state_to_bloch(state)
+ return _render_bloch_makie(bloch_vec; kwargs...)
+end
+
+end
diff --git a/src/QuantumToolbox.jl b/src/QuantumToolbox.jl
index d6735644d..c3d9be754 100644
--- a/src/QuantumToolbox.jl
+++ b/src/QuantumToolbox.jl
@@ -1,19 +1,13 @@
module QuantumToolbox
-# Re-export:
-# 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
-
-# other functions in LinearAlgebra
-import LinearAlgebra: BlasReal, BlasInt, BlasFloat, BlasComplex, checksquare
-import LinearAlgebra.BLAS: @blasfunc
+# Standard Julia libraries
+using LinearAlgebra
+import LinearAlgebra: BlasInt, BlasFloat, checksquare
import LinearAlgebra.LAPACK: hseqr!
+using SparseArrays
+import Statistics: mean, std
-# SciML packages (for OrdinaryDiffEq and LinearSolve)
+# SciML packages (for QobjEvo, OrdinaryDiffEq, and LinearSolve)
import SciMLBase:
solve,
solve!,
@@ -21,42 +15,82 @@ import SciMLBase:
reinit!,
remake,
u_modified!,
+ NullParameters,
+ ODEFunction,
+ SDEFunction,
ODEProblem,
+ SDEProblem,
EnsembleProblem,
+ EnsembleAlgorithm,
+ EnsembleSerial,
EnsembleThreads,
+ EnsembleSplitThreads,
+ EnsembleDistributed,
FullSpecialize,
CallbackSet,
ContinuousCallback,
- DiscreteCallback
-import SciMLOperators: MatrixOperator
+ DiscreteCallback,
+ AbstractSciMLProblem,
+ AbstractODEIntegrator,
+ AbstractODESolution
+import StochasticDiffEq: StochasticDiffEqAlgorithm, SRA2, SRIW1
+import SciMLOperators:
+ cache_operator,
+ iscached,
+ isconstant,
+ SciMLOperators,
+ AbstractSciMLOperator,
+ MatrixOperator,
+ ScalarOperator,
+ ScaledOperator,
+ AddedOperator,
+ IdentityOperator,
+ update_coefficients!,
+ concretize
import LinearSolve: LinearProblem, SciMLLinearSolveAlgorithm, KrylovJL_MINRES, KrylovJL_GMRES
import DiffEqBase: get_tstops
-import DiffEqCallbacks: PeriodicCallback, PresetTimeCallback, TerminateSteadyState
+import DiffEqCallbacks: PeriodicCallback, FunctionCallingCallback, FunctionCallingAffect, TerminateSteadyState
import OrdinaryDiffEqCore: OrdinaryDiffEqAlgorithm
import OrdinaryDiffEqTsit5: Tsit5
+import DiffEqNoiseProcess: RealWienerProcess!, RealWienerProcess
# other dependencies (in alphabetical order)
import ArrayInterface: allowed_getindex, allowed_setindex!
-import FFTW: fft, fftshift
+import Distributed: RemoteChannel
+import FFTW: fft, ifft, fftfreq, fftshift
import Graphs: connected_components, DiGraph
import IncompleteLU: ilu
+import LaTeXStrings: @L_str
import Pkg
-import Random
+import Random: AbstractRNG, default_rng, seed!
import SpecialFunctions: loggamma
-import StaticArraysCore: MVector
+import StaticArraysCore: SVector, MVector
+
+# Export functions from the other modules
+
+# LinearAlgebra
+export ishermitian, issymmetric, isposdef, dot, tr, svdvals, norm, normalize, normalize!, diag, Hermitian, Symmetric
-# Setting the number of threads to 1 allows
-# to achieve better performances for more massive parallelizations
-BLAS.set_num_threads(1)
+# SparseArrays
+export permute
+
+# SciMLOperators
+export cache_operator, iscached, isconstant
# Utility
+include("settings.jl")
include("utilities.jl")
include("versioninfo.jl")
include("progress_bar.jl")
include("linear_maps.jl")
# Quantum Object
+include("qobj/space.jl")
+include("qobj/energy_restricted.jl")
+include("qobj/dimensions.jl")
+include("qobj/quantum_object_base.jl")
include("qobj/quantum_object.jl")
+include("qobj/quantum_object_evo.jl")
include("qobj/boolean_functions.jl")
include("qobj/arithmetic_and_attributes.jl")
include("qobj/eigsolve.jl")
@@ -65,24 +99,38 @@ include("qobj/states.jl")
include("qobj/operators.jl")
include("qobj/superoperators.jl")
include("qobj/synonyms.jl")
-include("qobj/operator_sum.jl")
+include("qobj/block_diagonal_form.jl")
# time evolution
include("time_evolution/time_evolution.jl")
+include("time_evolution/callback_helpers/callback_helpers.jl")
+include("time_evolution/callback_helpers/sesolve_callback_helpers.jl")
+include("time_evolution/callback_helpers/mesolve_callback_helpers.jl")
+include("time_evolution/callback_helpers/mcsolve_callback_helpers.jl")
+include("time_evolution/callback_helpers/ssesolve_callback_helpers.jl")
+include("time_evolution/callback_helpers/smesolve_callback_helpers.jl")
include("time_evolution/mesolve.jl")
+include("time_evolution/brmesolve.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/smesolve.jl")
include("time_evolution/time_evolution_dynamical.jl")
# Others
-include("permutation.jl")
include("correlations.jl")
include("wigner.jl")
include("spin_lattice.jl")
include("arnoldi.jl")
+include("entropy.jl")
include("metrics.jl")
include("negativity.jl")
include("steadystate.jl")
+include("spectrum.jl")
+include("visualization.jl")
+
+# deprecated functions
+include("deprecated.jl")
end
diff --git a/src/correlations.jl b/src/correlations.jl
index 5f3c6d033..a8ac92f27 100644
--- a/src/correlations.jl
+++ b/src/correlations.jl
@@ -1,247 +1,155 @@
-export SpectrumSolver, FFTCorrelation, ExponentialSeries
-export correlation_3op_2t, correlation_2op_2t, correlation_2op_1t, spectrum
-
-abstract type SpectrumSolver end
-
-struct FFTCorrelation <: SpectrumSolver end
-
-struct ExponentialSeries{T<:Real,CALC_SS} <: SpectrumSolver
- tol::T
- ExponentialSeries(tol::T, calc_steadystate::Bool = false) where {T} = new{T,calc_steadystate}(tol)
+export correlation_3op_2t, correlation_3op_1t, correlation_2op_2t, correlation_2op_1t
+
+function _check_correlation_time_list(tlist::AbstractVector)
+ any(t -> t == 0, tlist) ||
+ throw(ArgumentError("The time list for calculating correlation function must contain the element `0`"))
+ all(>=(0), tlist) ||
+ throw(ArgumentError("All the elements in the time list for calculating correlation function must be positive."))
+ return nothing
end
-ExponentialSeries(; tol = 1e-14, calc_steadystate = false) = ExponentialSeries(tol, calc_steadystate)
-
@doc raw"""
- correlation_3op_2t(H::QuantumObject,
- ψ0::QuantumObject,
- t_l::AbstractVector,
- τ_l::AbstractVector,
+ correlation_3op_2t(H::AbstractQuantumObject,
+ ψ0::Union{Nothing,QuantumObject},
+ tlist::AbstractVector,
+ τlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
A::QuantumObject,
B::QuantumObject,
- C::QuantumObject,
- c_ops::Union{Nothing,AbstractVector}=nothing;
+ C::QuantumObject;
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)}``
+Returns the two-time correlation function of three operators ``\hat{A}``, ``\hat{B}`` and ``\hat{C}``: ``\left\langle \hat{A}(t) \hat{B}(t + \tau) \hat{C}(t) \right\rangle`` for a given initial state ``|\psi_0\rangle``.
-for a given initial state ``\ket{\psi_0}``.
+If the initial state `ψ0` is given as `nothing`, then the [`steadystate`](@ref) will be used as the initial state. Note that this is only implemented if `H` is constant ([`QuantumObject`](@ref)).
"""
function correlation_3op_2t(
- H::QuantumObject{<:AbstractArray{T1},HOpType},
- ψ0::QuantumObject{<:AbstractArray{T2},StateOpType},
- t_l::AbstractVector,
- τ_l::AbstractVector,
- A::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T4},OperatorQuantumObject},
- C::QuantumObject{<:AbstractArray{T5},OperatorQuantumObject},
- c_ops::Union{Nothing,AbstractVector} = nothing;
+ H::AbstractQuantumObject{HOpType},
+ ψ0::Union{Nothing,QuantumObject{StateOpType}},
+ tlist::AbstractVector,
+ τlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ C::QuantumObject{Operator};
kwargs...,
-) where {
- T1,
- T2,
- T3,
- T4,
- T5,
- HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},
-}
- (H.dims == ψ0.dims && H.dims == A.dims && H.dims == B.dims && H.dims == C.dims) ||
- throw(DimensionMismatch("The quantum objects are not of the same Hilbert dimension."))
-
- kwargs2 = (; kwargs...)
- kwargs2 = merge(kwargs2, (saveat = collect(t_l),))
- ρt = mesolve(H, ψ0, t_l, c_ops; kwargs2...).states
-
- corr = map((t, ρ) -> mesolve(H, C * ρ * A, τ_l .+ t, c_ops, e_ops = [B]; kwargs...).expect[1, :], t_l, ρt)
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator}}
+ # check tlist and τlist
+ _check_correlation_time_list(tlist)
+ _check_correlation_time_list(τlist)
- return corr
+ L = liouvillian(H, c_ops)
+ if ψ0 isa Nothing
+ ψ0 = steadystate(L)
+ end
+
+ check_dimensions(L, ψ0, A, B, C)
+
+ kwargs2 = merge((saveat = collect(tlist),), (; kwargs...))
+ ρt_list = mesolve(L, ψ0, tlist; kwargs2...).states
+
+ corr = map((t, ρt) -> mesolve(L, C * ρt * A, τlist .+ t, e_ops = [B]; kwargs...).expect[1, :], tlist, ρt_list)
+
+ # make the output correlation Matrix align with QuTiP
+ # 1st dimension corresponds to tlist
+ # 2nd dimension corresponds to τlist
+ return reduce(vcat, transpose.(corr))
end
@doc raw"""
- correlation_2op_2t(H::QuantumObject,
- ψ0::QuantumObject,
- t_l::AbstractVector,
- τ_l::AbstractVector,
+ correlation_3op_1t(H::AbstractQuantumObject,
+ ψ0::Union{Nothing,QuantumObject},
+ τlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
A::QuantumObject,
B::QuantumObject,
- c_ops::Union{Nothing,AbstractVector}=nothing;
- reverse::Bool=false,
+ C::QuantumObject;
kwargs...)
-Returns the two-times correlation function of two operators ``\hat{A}`` and ``\hat{B}``
-at different times: ``\expval{\hat{A}(t + \tau) \hat{B}(t)}``.
+Returns the two-time correlation function (with only one time coordinate ``\tau``) of three operators ``\hat{A}``, ``\hat{B}`` and ``\hat{C}``: ``\left\langle \hat{A}(0) \hat{B}(\tau) \hat{C}(0) \right\rangle`` for a given initial state ``|\psi_0\rangle``.
-When `reverse=true`, the correlation function is calculated as ``\expval{\hat{A}(t) \hat{B}(t + \tau)}``.
+If the initial state `ψ0` is given as `nothing`, then the [`steadystate`](@ref) will be used as the initial state. Note that this is only implemented if `H` is constant ([`QuantumObject`](@ref)).
"""
-function correlation_2op_2t(
- H::QuantumObject{<:AbstractArray{T1},HOpType},
- ψ0::QuantumObject{<:AbstractArray{T2},StateOpType},
- t_l::AbstractVector,
- τ_l::AbstractVector,
- A::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T4},OperatorQuantumObject},
- c_ops::Union{Nothing,AbstractVector} = nothing;
- reverse::Bool = false,
+function correlation_3op_1t(
+ H::AbstractQuantumObject{HOpType},
+ ψ0::Union{Nothing,QuantumObject{StateOpType}},
+ τlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ C::QuantumObject{Operator};
kwargs...,
-) where {
- T1,
- T2,
- T3,
- T4,
- HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},
-}
- C = eye(prod(H.dims), dims = H.dims)
- if reverse
- corr = correlation_3op_2t(H, ψ0, t_l, τ_l, A, B, C, c_ops; kwargs...)
- else
- corr = correlation_3op_2t(H, ψ0, t_l, τ_l, C, A, B, c_ops; kwargs...)
- end
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator}}
+ corr = correlation_3op_2t(H, ψ0, [0], τlist, c_ops, A, B, C; kwargs...)
- return reduce(hcat, corr)
+ return corr[1, :] # 1 means tlist[1] = 0
end
@doc raw"""
- correlation_2op_1t(H::QuantumObject,
- ψ0::QuantumObject,
- τ_l::AbstractVector,
+ correlation_2op_2t(H::AbstractQuantumObject,
+ ψ0::Union{Nothing,QuantumObject},
+ tlist::AbstractVector,
+ τlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
A::QuantumObject,
- B::QuantumObject,
- c_ops::Union{Nothing,AbstractVector}=nothing;
+ B::QuantumObject;
reverse::Bool=false,
kwargs...)
-Returns the one-time correlation function of two operators ``\hat{A}`` and ``\hat{B}`` at different times ``\expval{\hat{A}(\tau) \hat{B}(0)}``.
+Returns the two-time correlation function of two operators ``\hat{A}`` and ``\hat{B}`` : ``\left\langle \hat{A}(t + \tau) \hat{B}(t) \right\rangle`` for a given initial state ``|\psi_0\rangle``.
-When `reverse=true`, the correlation function is calculated as ``\expval{\hat{A}(0) \hat{B}(\tau)}``.
+If the initial state `ψ0` is given as `nothing`, then the [`steadystate`](@ref) will be used as the initial state. Note that this is only implemented if `H` is constant ([`QuantumObject`](@ref)).
+
+When `reverse=true`, the correlation function is calculated as ``\left\langle \hat{A}(t) \hat{B}(t + \tau) \right\rangle``.
"""
-function correlation_2op_1t(
- H::QuantumObject{<:AbstractArray{T1},HOpType},
- ψ0::QuantumObject{<:AbstractArray{T2},StateOpType},
- τ_l::AbstractVector,
- A::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T4},OperatorQuantumObject},
- c_ops::Union{Nothing,AbstractVector} = nothing;
+function correlation_2op_2t(
+ H::AbstractQuantumObject{HOpType},
+ ψ0::Union{Nothing,QuantumObject{StateOpType}},
+ tlist::AbstractVector,
+ τlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator};
reverse::Bool = false,
kwargs...,
-) where {
- T1,
- T2,
- T3,
- T4,
- HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},
-}
- corr = correlation_2op_2t(H, ψ0, [0], τ_l, A, B, c_ops; reverse = reverse, kwargs...)
-
- return corr[:, 1]
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator}}
+ C = eye(prod(H.dimensions), dims = H.dimensions)
+ if reverse
+ corr = correlation_3op_2t(H, ψ0, tlist, τlist, c_ops, A, B, C; kwargs...)
+ else
+ corr = correlation_3op_2t(H, ψ0, tlist, τlist, c_ops, C, A, B; kwargs...)
+ end
+
+ return corr
end
@doc raw"""
- spectrum(H::QuantumObject,
- ω_list::AbstractVector,
- A::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject},
- c_ops::Union{Nothing,AbstractVector}=nothing;
- solver::MySolver=ExponentialSeries(),
+ correlation_2op_1t(H::AbstractQuantumObject,
+ ψ0::Union{Nothing,QuantumObject},
+ τlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
+ A::QuantumObject,
+ B::QuantumObject;
+ reverse::Bool=false,
kwargs...)
-Returns the emission spectrum
+Returns the two-time correlation function (with only one time coordinate ``\tau``) of two operators ``\hat{A}`` and ``\hat{B}`` : ``\left\langle \hat{A}(\tau) \hat{B}(0) \right\rangle`` for a given initial state ``|\psi_0\rangle``.
-```math
-S(\omega) = \int_{-\infty}^\infty \expval{\hat{A}(\tau) \hat{B}(0)} e^{-i \omega \tau} d \tau
-```
-"""
-function spectrum(
- H::QuantumObject{MT1,HOpType},
- ω_list::AbstractVector,
- A::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject},
- c_ops::Union{Nothing,AbstractVector} = nothing;
- solver::MySolver = ExponentialSeries(),
- kwargs...,
-) where {
- MT1<:AbstractMatrix,
- T2,
- T3,
- HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- MySolver<:SpectrumSolver,
-}
- return _spectrum(H, ω_list, A, B, c_ops, solver; kwargs...)
-end
-
-function _spectrum(
- H::QuantumObject{<:AbstractArray{T1},HOpType},
- ω_list::AbstractVector,
- A::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject},
- c_ops,
- solver::FFTCorrelation;
- kwargs...,
-) where {T1,T2,T3,HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
- Nsamples = length(ω_list)
- ω_max = abs(maximum(ω_list))
- dω = 2 * ω_max / (Nsamples - 1)
- ω_l = -ω_max:dω:ω_max
-
- T = 2π / (ω_l[2] - ω_l[1])
- τ_l = range(0, T, length = length(ω_l))
-
- ρss = steadystate(H, c_ops)
- corr = correlation_2op_1t(H, ρss, τ_l, A, B, c_ops; kwargs...)
-
- S = fftshift(fft(corr)) / length(τ_l)
-
- return ω_l, 2 .* real.(S)
-end
-
-function _spectrum_get_rates_vecs_ss(L, solver::ExponentialSeries{T,true}) where {T}
- result = eigen(L)
- rates, vecs = result.values, result.vectors
-
- return rates, vecs, steadystate(L).data
-end
-
-function _spectrum_get_rates_vecs_ss(L, solver::ExponentialSeries{T,false}) where {T}
- result = eigen(L)
- rates, vecs = result.values, result.vectors
+If the initial state `ψ0` is given as `nothing`, then the [`steadystate`](@ref) will be used as the initial state. Note that this is only implemented if `H` is constant ([`QuantumObject`](@ref)).
- ss_idx = findmin(abs2, rates)[2]
- ρss = vec2mat(@view(vecs[:, ss_idx]))
- ρss = (ρss + ρss') / 2
- ρss ./= tr(ρss)
-
- return rates, vecs, ρss
-end
-
-function _spectrum(
- H::QuantumObject{<:AbstractArray{T1},HOpType},
- ω_list::AbstractVector,
- A::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject},
- c_ops,
- solver::ExponentialSeries;
+When `reverse=true`, the correlation function is calculated as ``\left\langle \hat{A}(0) \hat{B}(\tau) \right\rangle``.
+"""
+function correlation_2op_1t(
+ H::AbstractQuantumObject{HOpType},
+ ψ0::Union{Nothing,QuantumObject{StateOpType}},
+ τlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator};
+ reverse::Bool = false,
kwargs...,
-) where {T1,T2,T3,HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
- (H.dims == A.dims == B.dims) || throw(DimensionMismatch("The dimensions of H, A and B must be the same"))
-
- L = liouvillian(H, c_ops)
-
- ω_l = ω_list
-
- rates, vecs, ρss = _spectrum_get_rates_vecs_ss(L, solver)
-
- ρ0 = B.data * ρss
- v = vecs \ mat2vec(ρ0)
-
- amps = map(i -> v[i] * tr(A.data * vec2mat(@view(vecs[:, i]))), eachindex(rates))
- idxs = findall(x -> abs(x) > solver.tol, amps)
- amps, rates = amps[idxs], rates[idxs]
-
- # spec = map(ω -> 2 * real(sum(@. amps * (1 / (1im * ω - rates)))), ω_l)
- amps_rates = zip(amps, rates)
- spec = map(ω -> 2 * real(sum(x -> x[1] / (1im * ω - x[2]), amps_rates)), ω_l)
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator}}
+ corr = correlation_2op_2t(H, ψ0, [0], τlist, c_ops, A, B; reverse = reverse, kwargs...)
- return ω_l, spec
+ return corr[1, :] # 1 means tlist[1] = 0
end
diff --git a/src/deprecated.jl b/src/deprecated.jl
new file mode 100644
index 000000000..f0a095bf0
--- /dev/null
+++ b/src/deprecated.jl
@@ -0,0 +1,87 @@
+#=
+This file gathers all the deprecated names (structures, functions, or variables) which will be removed in the future major release.
+
+- Before the major release, the deprecated names will just throw errors when they are called.
+- If the deprecated names were once exported, we will still export them here until next major release.
+- If we decide to push a major release, cleanup this file.
+
+Example:
+
+export deprecated_foo
+
+function deprecated_foo(args...; kwargs...)
+ error("`deprecated_foo` has been deprecated and will be removed in next major release, please use `new_foo` instead.")
+end
+=#
+
+export FFTCorrelation
+export sparse_to_dense, dense_to_sparse
+
+FFTCorrelation() = error(
+ "`FFTCorrelation` has been deprecated and will be removed in next major release, please use `spectrum_correlation_fft` to calculate the spectrum with FFT method instead.",
+)
+
+sparse_to_dense(args...) = error(
+ "`sparse_to_dense` has been deprecated and will be removed in next major release, please use `to_dense` instead.",
+)
+dense_to_sparse(args...) = error(
+ "`dense_to_sparse` has been deprecated and will be removed in next major release, please use `to_sparse` instead.",
+)
+
+correlation_3op_2t(
+ H::QuantumObject{HOpType},
+ ψ0::QuantumObject{StateOpType},
+ t_l::AbstractVector,
+ τ_l::AbstractVector,
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ C::QuantumObject{Operator},
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ kwargs...,
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator}} = error(
+ "The parameter order of `correlation_3op_2t` has been changed, please use `?correlation_3op_2t` to check the updated docstring.",
+)
+
+correlation_3op_1t(
+ H::QuantumObject{HOpType},
+ ψ0::QuantumObject{StateOpType},
+ τ_l::AbstractVector,
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ C::QuantumObject{Operator},
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ kwargs...,
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator}} = error(
+ "The parameter order of `correlation_3op_1t` has been changed, please use `?correlation_3op_1t` to check the updated docstring.",
+)
+
+correlation_2op_2t(
+ H::QuantumObject{HOpType},
+ ψ0::QuantumObject{StateOpType},
+ t_l::AbstractVector,
+ τ_l::AbstractVector,
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ reverse::Bool = false,
+ kwargs...,
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator}} = error(
+ "The parameter order of `correlation_2op_2t` has been changed, please use `?correlation_2op_2t` to check the updated docstring.",
+)
+
+correlation_2op_1t(
+ H::QuantumObject{HOpType},
+ ψ0::QuantumObject{StateOpType},
+ τ_l::AbstractVector,
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ reverse::Bool = false,
+ kwargs...,
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator}} = error(
+ "The parameter order of `correlation_2op_1t` has been changed, please use `?correlation_2op_1t` to check the updated docstring.",
+)
+
+MultiSiteOperator(dims::Union{AbstractVector,Tuple}, pairs::Pair{<:Integer,<:QuantumObject}...) = error(
+ "`MultiSiteOperator` has been deprecated and will be removed in next major release, please use `multisite_operator` instead.",
+)
diff --git a/src/entropy.jl b/src/entropy.jl
new file mode 100644
index 000000000..575ea3209
--- /dev/null
+++ b/src/entropy.jl
@@ -0,0 +1,241 @@
+#=
+Entropy related functions and some entanglement measures.
+=#
+
+export entropy_vn, entropy_relative, entropy_linear, entropy_mutual, entropy_conditional
+export entanglement, concurrence
+
+@doc raw"""
+ entropy_vn(ρ::QuantumObject; base::Int=0, tol::Real=1e-15)
+
+Calculates the [Von Neumann entropy](https://en.wikipedia.org/wiki/Von_Neumann_entropy) ``S = - \textrm{Tr} \left[ \hat{\rho} \log \left( \hat{\rho} \right) \right]``, where ``\hat{\rho}`` is the density matrix of the system.
+
+# Notes
+
+- `ρ` is the quantum state, can be either a [`Ket`](@ref) or an [`Operator`](@ref).
+- `base` specifies the base of the logarithm to use, and when using the default value `0`, the natural logarithm is used.
+- `tol` describes the absolute tolerance for detecting the zero-valued eigenvalues of the density matrix ``\hat{\rho}``.
+
+# Examples
+
+Pure state:
+```jldoctest
+julia> ψ = fock(2,0)
+
+Quantum Object: type=Ket() dims=[2] size=(2,)
+2-element Vector{ComplexF64}:
+ 1.0 + 0.0im
+ 0.0 + 0.0im
+
+julia> entropy_vn(ψ, base=2)
+-0.0
+```
+
+Mixed state:
+```jldoctest
+julia> ρ = maximally_mixed_dm(2)
+
+Quantum Object: type=Operator() dims=[2] size=(2, 2) ishermitian=true
+2×2 Diagonal{ComplexF64, Vector{ComplexF64}}:
+ 0.5-0.0im ⋅
+ ⋅ 0.5-0.0im
+
+julia> entropy_vn(ρ, base=2)
+1.0
+```
+"""
+function entropy_vn(ρ::QuantumObject{ObjType}; base::Int = 0, tol::Real = 1e-15) where {ObjType<:Union{Ket,Operator}}
+ T = eltype(ρ)
+ vals = eigenenergies(ket2dm(ρ))
+ 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(mapreduce(*,+,nzvals,logvals))
+end
+
+@doc raw"""
+ entropy_relative(ρ::QuantumObject, σ::QuantumObject; base::Int=0, tol::Real=1e-15)
+
+Calculates the [quantum relative entropy](https://en.wikipedia.org/wiki/Quantum_relative_entropy) of ``\hat{\rho}`` with respect to ``\hat{\sigma}``: ``D(\hat{\rho}||\hat{\sigma}) = \textrm{Tr} \left[ \hat{\rho} \log \left( \hat{\rho} \right) \right] - \textrm{Tr} \left[ \hat{\rho} \log \left( \hat{\sigma} \right) \right]``.
+
+# Notes
+
+- `ρ` is a quantum state, can be either a [`Ket`](@ref) or an [`Operator`](@ref).
+- `σ` is a quantum state, can be either a [`Ket`](@ref) or an [`Operator`](@ref).
+- `base` specifies the base of the logarithm to use, and when using the default value `0`, the natural logarithm is used.
+- `tol` describes the absolute tolerance for detecting the zero-valued eigenvalues of the density matrix ``\hat{\rho}``.
+
+# References
+- [Nielsen-Chuang2011; section 11.3.1, page 511](@citet)
+"""
+function entropy_relative(
+ ρ::QuantumObject{ObjType1},
+ σ::QuantumObject{ObjType2};
+ base::Int = 0,
+ tol::Real = 1e-15,
+) where {ObjType1<:Union{Ket,Operator},ObjType2<:Union{Ket,Operator}}
+ check_dimensions(ρ, σ)
+
+ # the logic of this code follows the detail given in the reference of the docstring
+ # consider the eigen decompositions:
+ # ρ = Σ_i p_i |i⟩⟨i|
+ # σ = Σ_j q_j |j⟩⟨j|
+ ρ_result = eigenstates(ket2dm(ρ))
+ σ_result = eigenstates(ket2dm(σ))
+
+ # make sure all p_i and q_j are real
+ any(p_i -> imag(p_i) >= tol, ρ_result.values) && error("Input `ρ` has non-real eigenvalues.")
+ any(q_j -> imag(q_j) >= tol, σ_result.values) && error("Input `σ` has non-real eigenvalues.")
+ p = real(ρ_result.values)
+ q = real(σ_result.values)
+ Uρ = ρ_result.vectors
+ Uσ = σ_result.vectors
+
+ # create P_ij matrix (all elements should be real)
+ P = abs2.(Uρ' * Uσ) # this equals to ⟨i|j⟩⟨j|i⟩
+
+ # return +∞ if kernel of σ overlaps with support of ρ, i.e., supp(p) ⊆ supp(q)
+ # That is, if σ is not full rank, S(ρ||σ) = +∞
+ # note that, one special case is that S(ρ||σ) = 0 (if ρ == σ)
+ ((transpose(p .>= tol) * (P .>= tol) * (q .< tol)) == 0) || return Inf
+
+ # Avoid -∞ from log(0), these terms will be multiplied by zero later anyway
+ replace!(q_j -> abs(q_j) < tol ? 1 : q_j, q)
+ p_vals = filter(p_i -> abs(p_i) >= tol, p)
+
+ if base == 0
+ log_p = log.(p_vals)
+ log_q = log.(q)
+ else
+ log_p = log.(base, p_vals)
+ log_q = log.(base, q)
+ end
+
+ # the relative entropy is guaranteed to be ≥ 0
+ # so we calculate the value to 0 to avoid small violations of the lower bound.
+ return max(0.0, dot(p_vals, log_p) - dot(p, P, log_q)) # use 0.0 to make sure it always return value in Float-type
+end
+
+@doc raw"""
+ entropy_linear(ρ::QuantumObject)
+
+Calculates the quantum linear entropy ``S_L = 1 - \textrm{Tr} \left[ \hat{\rho}^2 \right]``, where ``\hat{\rho}`` is the density matrix of the system.
+
+Note that `ρ` can be either a [`Ket`](@ref) or an [`Operator`](@ref).
+"""
+entropy_linear(ρ::QuantumObject{ObjType}) where {ObjType<:Union{Ket,Operator}} = 1.0 - purity(ρ) # use 1.0 to make sure it always return value in Float-type
+
+@doc raw"""
+ entropy_mutual(ρAB::QuantumObject, selA, selB; kwargs...)
+
+Calculates the [quantum mutual information](https://en.wikipedia.org/wiki/Quantum_mutual_information) ``I(A:B) = S(\hat{\rho}_A) + S(\hat{\rho}_B) - S(\hat{\rho}_{AB})`` between subsystems ``A`` and ``B``.
+
+Here, ``S`` is the [Von Neumann entropy](https://en.wikipedia.org/wiki/Von_Neumann_entropy), ``\hat{\rho}_{AB}`` is the density matrix of the entire system, ``\hat{\rho}_A = \textrm{Tr}_B \left[ \hat{\rho}_{AB} \right]``, ``\hat{\rho}_B = \textrm{Tr}_A \left[ \hat{\rho}_{AB} \right]``.
+
+# Notes
+
+- `ρAB` can be either a [`Ket`](@ref) or an [`Operator`](@ref).
+- `selA` specifies the indices of the sub-system `A` in `ρAB.dimensions`. See also [`ptrace`](@ref).
+- `selB` specifies the indices of the sub-system `B` in `ρAB.dimensions`. See also [`ptrace`](@ref).
+- `kwargs` are the keyword arguments for calculating Von Neumann entropy. See also [`entropy_vn`](@ref).
+"""
+function entropy_mutual(
+ ρAB::QuantumObject{ObjType,<:AbstractDimensions{N,N}},
+ selA::Union{Int,AbstractVector{Int},Tuple},
+ selB::Union{Int,AbstractVector{Int},Tuple};
+ kwargs...,
+) where {ObjType<:Union{Ket,Operator},N}
+ # check if selA and selB matches the dimensions of ρAB
+ sel_A_B = (selA..., selB...)
+ (length(sel_A_B) != N) && throw(
+ ArgumentError(
+ "The indices in `selA = $(selA)` and `selB = $(selB)` does not match the given QuantumObject which has $N sub-systems",
+ ),
+ )
+ allunique(sel_A_B) || throw(ArgumentError("Duplicate selection indices in `selA = $(selA)` and `selB = $(selB)`"))
+
+ ρA = ptrace(ρAB, selA)
+ ρB = ptrace(ρAB, selB)
+ return entropy_vn(ρA; kwargs...) + entropy_vn(ρB; kwargs...) - entropy_vn(ρAB; kwargs...)
+end
+
+@doc raw"""
+ entropy_conditional(ρAB::QuantumObject, selB; kwargs...)
+
+Calculates the [conditional quantum entropy](https://en.wikipedia.org/wiki/Conditional_quantum_entropy) with respect to sub-system ``B``: ``S(A|B) = S(\hat{\rho}_{AB}) - S(\hat{\rho}_{B})``.
+
+Here, ``S`` is the [Von Neumann entropy](https://en.wikipedia.org/wiki/Von_Neumann_entropy), ``\hat{\rho}_{AB}`` is the density matrix of the entire system, and ``\hat{\rho}_B = \textrm{Tr}_A \left[ \hat{\rho}_{AB} \right]``.
+
+# Notes
+
+- `ρAB` can be either a [`Ket`](@ref) or an [`Operator`](@ref).
+- `selB` specifies the indices of the sub-system `B` in `ρAB.dimensions`. See also [`ptrace`](@ref).
+- `kwargs` are the keyword arguments for calculating Von Neumann entropy. See also [`entropy_vn`](@ref).
+"""
+entropy_conditional(
+ ρAB::QuantumObject{ObjType,<:AbstractDimensions{N,N}},
+ selB::Union{Int,AbstractVector{Int},Tuple};
+ kwargs...,
+) where {ObjType<:Union{Ket,Operator},N} = entropy_vn(ρAB; kwargs...) - entropy_vn(ptrace(ρAB, selB); kwargs...)
+
+@doc raw"""
+ entanglement(ρ::QuantumObject, sel; kwargs...)
+
+Calculates the [entanglement entropy](https://en.wikipedia.org/wiki/Entropy_of_entanglement) by doing the partial trace of `ρ`, selecting only the dimensions with the indices contained in the `sel` vector, and then use the Von Neumann entropy [`entropy_vn`](@ref).
+
+# Notes
+
+- `ρ` can be either a [`Ket`](@ref) or an [`Operator`](@ref). But should be a pure state.
+- `sel` specifies the indices of the remaining sub-system. See also [`ptrace`](@ref).
+- `kwargs` are the keyword arguments for calculating Von Neumann entropy. See also [`entropy_vn`](@ref).
+"""
+function entanglement(
+ ρ::QuantumObject{OpType},
+ sel::Union{Int,AbstractVector{Int},Tuple},
+ kwargs...,
+) where {OpType<:Union{Ket,Operator}}
+ p = purity(ρ)
+ isapprox(p, 1; atol = 1e-2) || throw(
+ ArgumentError(
+ "The entanglement entropy only works for normalized pure state, the purity of the given state: $(p) ≉ 1",
+ ),
+ )
+
+ ρ_tr = ptrace(ρ, sel)
+ val = entropy_vn(ρ_tr; kwargs...)
+ return max(0.0, val) # use 0.0 to make sure it always return value in Float-type
+end
+
+@doc raw"""
+ concurrence(ρ::QuantumObject)
+
+Calculate the [concurrence](https://en.wikipedia.org/wiki/Concurrence_(quantum_computing)) for a two-qubit state.
+
+# Notes
+
+- `ρ` can be either a [`Ket`](@ref) or an [`Operator`](@ref).
+
+# References
+
+- [Hill-Wootters1997](@citet)
+"""
+function concurrence(ρ::QuantumObject{OpType}) where {OpType<:Union{Ket,Operator}}
+ (ρ.dimensions == Dimensions((Space(2), Space(2)))) || throw(
+ ArgumentError(
+ "The `concurrence` only works for a two-qubit state, invalid dims = $(_get_dims_string(ρ.dimensions)).",
+ ),
+ )
+
+ _ρ = ket2dm(ρ).data
+ σy = sigmay()
+ σyσy = kron(σy, σy).data
+ ρ_tilde = σyσy * conj(_ρ) * σyσy
+
+ # we use the alternative way to calculate concurrence (more efficient)
+ # calculate the square root of each eigenvalues (in decreasing order) of the non-Hermitian matrix: ρ * ρ_tilde
+ # note that we add abs here to avoid problems with sqrt for very small negative numbers
+ λ = sqrt.(abs.(real(eigvals(_ρ * ρ_tilde; sortby = x -> -real(x)))))
+
+ return max(0.0, λ[1] - λ[2] - λ[3] - λ[4]) # use 0.0 to make sure it always return value in Float-type
+end
diff --git a/src/linear_maps.jl b/src/linear_maps.jl
index 8afd88e70..a5e4082aa 100644
--- a/src/linear_maps.jl
+++ b/src/linear_maps.jl
@@ -18,7 +18,7 @@ A **linear map** is a transformation `L` that satisfies:
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.
+It is typically represented as a matrix with dimensions given by `size`, and this abstract type helps to define this map when the matrix is not explicitly available.
## Methods
@@ -30,7 +30,7 @@ It is typically represented as a matrix with dimensions given by `size`, and thi
As an example, we now define the linear map used in the [`eigsolve_al`](@ref) function for Arnoldi-Lindblad eigenvalue solver:
-```julia-repl
+```julia
struct ArnoldiLindbladIntegratorMap{T,TS,TI} <: AbstractLinearMap{T,TS}
elty::Type{T}
size::TS
diff --git a/src/metrics.jl b/src/metrics.jl
index d257d24ba..90c22b01a 100644
--- a/src/metrics.jl
+++ b/src/metrics.jl
@@ -2,127 +2,107 @@
Functions for calculating metrics (distance measures) between states and operators.
=#
-export entropy_vn, entanglement, tracedist, fidelity
+export fidelity
+export tracedist, hilbert_dist, hellinger_dist
+export bures_dist, bures_angle
@doc raw"""
- entropy_vn(ρ::QuantumObject; base::Int=0, tol::Real=1e-15)
-
-Calculates the [Von Neumann entropy](https://en.wikipedia.org/wiki/Von_Neumann_entropy)
-``S = - \Tr \left[ \hat{\rho} \log \left( \hat{\rho} \right) \right]`` where ``\hat{\rho}``
-is the density matrix of the system.
-
-The `base` parameter specifies the base of the logarithm to use, and when using the default value 0,
-the natural logarithm is used. The `tol` parameter
-describes the absolute tolerance for detecting the zero-valued eigenvalues of the density
-matrix ``\hat{\rho}``.
-
-# Examples
-
-Pure state:
-```
-julia> ψ = fock(2,0)
-Quantum Object: type=Ket dims=[2] size=(2,)
-2-element Vector{ComplexF64}:
- 1.0 + 0.0im
- 0.0 + 0.0im
-
-julia> ρ = ket2dm(ψ)
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
-2×2 Matrix{ComplexF64}:
- 1.0+0.0im 0.0+0.0im
- 0.0+0.0im 0.0+0.0im
-
-julia> entropy_vn(ρ, base=2)
--0.0
-```
-
-Mixed state:
-```
-julia> ρ = maximally_mixed_dm(2)
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
-2×2 Diagonal{ComplexF64, Vector{ComplexF64}}:
- 0.5-0.0im ⋅
- ⋅ 0.5-0.0im
-
-julia> entropy_vn(ρ, base=2)
-1.0
-```
-"""
-function entropy_vn(
- ρ::QuantumObject{<:AbstractArray{T},OperatorQuantumObject};
- base::Int = 0,
- tol::Real = 1e-15,
-) where {T}
- 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(mapreduce(*, +, nzvals, logvals))
-end
+ fidelity(ρ::QuantumObject, σ::QuantumObject)
-"""
- entanglement(QO::QuantumObject, sel::Union{Int,AbstractVector{Int},Tuple})
+Calculate the fidelity of two [`QuantumObject`](@ref):
+``F(\hat{\rho}, \hat{\sigma}) = \textrm{Tr} \sqrt{\sqrt{\hat{\rho}} \hat{\sigma} \sqrt{\hat{\rho}}}``
-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).
+Here, the definition is from [Nielsen-Chuang2011](@citet). It is the square root of the fidelity defined in [Jozsa1994](@citet).
+
+Note that `ρ` and `σ` must be either [`Ket`](@ref) or [`Operator`](@ref).
"""
-function entanglement(
- QO::QuantumObject{<:AbstractArray{T},OpType},
- 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
+function fidelity(ρ::QuantumObject{Operator}, σ::QuantumObject{Operator})
+ sqrt_ρ = sqrt(ρ)
+ eigval = abs.(eigvals(sqrt_ρ * σ * sqrt_ρ))
+ return sum(sqrt, eigval)
end
-entanglement(QO::QuantumObject, sel::Int) = entanglement(QO, (sel,))
+fidelity(ρ::QuantumObject{Operator}, ψ::QuantumObject{Ket}) = sqrt(abs(expect(ρ, ψ)))
+fidelity(ψ::QuantumObject{Ket}, σ::QuantumObject{Operator}) = fidelity(σ, ψ)
+fidelity(ψ::QuantumObject{Ket}, ϕ::QuantumObject{Ket}) = abs(dot(ψ, ϕ))
@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).
"""
tracedist(
- ρ::QuantumObject{<:AbstractArray{T1},ObjType1},
- σ::QuantumObject{<:AbstractArray{T2},ObjType2},
-) where {
- T1,
- T2,
- ObjType1<:Union{KetQuantumObject,OperatorQuantumObject},
- ObjType2<:Union{KetQuantumObject,OperatorQuantumObject},
-} = norm(ket2dm(ρ) - ket2dm(σ), 1) / 2
+ ρ::QuantumObject{ObjType1},
+ σ::QuantumObject{ObjType2},
+) where {ObjType1<:Union{Ket,Operator},ObjType2<:Union{Ket,Operator}} = norm(ket2dm(ρ) - ket2dm(σ), 1) / 2
@doc raw"""
- fidelity(ρ::QuantumObject, σ::QuantumObject)
+ hilbert_dist(ρ::QuantumObject, σ::QuantumObject)
-Calculate the fidelity of two [`QuantumObject`](@ref):
-``F(\rho, \sigma) = \textrm{Tr} \sqrt{\sqrt{\rho} \sigma \sqrt{\rho}}``
+Calculates the Hilbert-Schmidt distance between two [`QuantumObject`](@ref):
+``D_{HS}(\hat{\rho}, \hat{\sigma}) = \textrm{Tr}\left[\hat{A}^\dagger \hat{A}\right]``, where ``\hat{A} = \hat{\rho} - \hat{\sigma}``.
+
+Note that `ρ` and `σ` must be either [`Ket`](@ref) or [`Operator`](@ref).
+
+# References
+- [Vedral-Plenio1998](@citet)
+"""
+function hilbert_dist(
+ ρ::QuantumObject{ObjType1},
+ σ::QuantumObject{ObjType2},
+) where {ObjType1<:Union{Ket,Operator},ObjType2<:Union{Ket,Operator}}
+ check_dimensions(ρ, σ)
+
+ A = ket2dm(ρ) - ket2dm(σ)
+ return tr(A' * A)
+end
+
+@doc raw"""
+ hellinger_dist(ρ::QuantumObject, σ::QuantumObject)
-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).
+Calculates the [Hellinger distance](https://en.wikipedia.org/wiki/Hellinger_distance) between two [`QuantumObject`](@ref):
+``D_H(\hat{\rho}, \hat{\sigma}) = \sqrt{2 - 2 \textrm{Tr}\left(\sqrt{\hat{\rho}}\sqrt{\hat{\sigma}}\right)}``
Note that `ρ` and `σ` must be either [`Ket`](@ref) or [`Operator`](@ref).
+
+# References
+- [Spehner2017](@citet)
"""
-function fidelity(
- ρ::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- σ::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
-) where {T1,T2}
- sqrt_ρ = sqrt(ρ)
- eigval = abs.(eigvals(sqrt_ρ * σ * sqrt_ρ))
- return sum(sqrt, eigval)
+function hellinger_dist(
+ ρ::QuantumObject{ObjType1},
+ σ::QuantumObject{ObjType2},
+) where {ObjType1<:Union{Ket,Operator},ObjType2<:Union{Ket,Operator}}
+ # Ket (pure state) doesn't need to do square root
+ sqrt_ρ = isket(ρ) ? ket2dm(ρ) : sqrt(ρ)
+ sqrt_σ = isket(σ) ? ket2dm(σ) : sqrt(σ)
+
+ # `max` is to avoid numerical instabilities
+ # it happens when ρ = σ, sum(eigvals) might be slightly larger than 1
+ return sqrt(2.0 * max(0.0, 1.0 - sum(real, eigvals(sqrt_ρ * sqrt_σ))))
end
-fidelity(
- ρ::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- ψ::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
-) where {T1,T2} = sqrt(abs(expect(ρ, ψ)))
-fidelity(
- ψ::QuantumObject{<:AbstractArray{T1},KetQuantumObject},
- σ::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
-) where {T1,T2} = fidelity(σ, ψ)
-fidelity(
- ψ::QuantumObject{<:AbstractArray{T1},KetQuantumObject},
- ϕ::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
-) where {T1,T2} = abs(dot(ψ, ϕ))
+
+@doc raw"""
+ bures_dist(ρ::QuantumObject, σ::QuantumObject)
+
+Calculate the [Bures distance](https://en.wikipedia.org/wiki/Bures_metric) between two [`QuantumObject`](@ref):
+``D_B(\hat{\rho}, \hat{\sigma}) = \sqrt{2 \left(1 - F(\hat{\rho}, \hat{\sigma}) \right)}``
+
+Here, the definition of [`fidelity`](@ref) ``F`` is from [Nielsen-Chuang2011](@citet). It is the square root of the fidelity defined in [Jozsa1994](@citet).
+
+Note that `ρ` and `σ` must be either [`Ket`](@ref) or [`Operator`](@ref).
+"""
+bures_dist(ρ::QuantumObject, σ::QuantumObject) = sqrt(2 * (1 - fidelity(ρ, σ)))
+
+@doc raw"""
+ bures_angle(ρ::QuantumObject, σ::QuantumObject)
+
+Calculate the [Bures angle](https://en.wikipedia.org/wiki/Bures_metric) between two [`QuantumObject`](@ref):
+``D_A(\hat{\rho}, \hat{\sigma}) = \arccos\left(F(\hat{\rho}, \hat{\sigma})\right)``
+
+Here, the definition of [`fidelity`](@ref) ``F`` is from [Nielsen-Chuang2011](@citet). It is the square root of the fidelity defined in [Jozsa1994](@citet).
+
+Note that `ρ` and `σ` must be either [`Ket`](@ref) or [`Operator`](@ref).
+"""
+bures_angle(ρ::QuantumObject, σ::QuantumObject) = acos(fidelity(ρ, σ))
diff --git a/src/negativity.jl b/src/negativity.jl
index e3669e383..286ce37a0 100644
--- a/src/negativity.jl
+++ b/src/negativity.jl
@@ -3,12 +3,12 @@ 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)).
+- `ρ::QuantumObject`: The density matrix (`ρ.type` must be [`Operator`](@ref)).
- `subsys::Int`: an index that indicates which subsystem to compute the negativity for.
- `logarithmic::Bool`: choose whether to calculate logarithmic negativity or not. Default as `false`
@@ -17,9 +17,10 @@ and ``\Vert X \Vert_1=\textrm{Tr}\sqrt{X^\dagger X}`` is the trace norm.
# Examples
-```
+```jldoctest
julia> Ψ = bell_state(0, 0)
-Quantum Object: type=Ket dims=[2, 2] size=(4,)
+
+Quantum Object: type=Ket() dims=[2, 2] size=(4,)
4-element Vector{ComplexF64}:
0.7071067811865475 + 0.0im
0.0 + 0.0im
@@ -27,19 +28,20 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,)
0.7071067811865475 + 0.0im
julia> ρ = ket2dm(Ψ)
-Quantum Object: type=Operator dims=[2, 2] size=(4, 4) ishermitian=true
+
+Quantum Object: type=Operator() dims=[2, 2] size=(4, 4) ishermitian=true
4×4 Matrix{ComplexF64}:
0.5+0.0im 0.0+0.0im 0.0+0.0im 0.5+0.0im
0.0+0.0im 0.0+0.0im 0.0+0.0im 0.0+0.0im
0.0+0.0im 0.0+0.0im 0.0+0.0im 0.0+0.0im
0.5+0.0im 0.0+0.0im 0.0+0.0im 0.5+0.0im
-julia> negativity(ρ, 2)
-0.4999999999999998
+julia> round(negativity(ρ, 2), digits=2)
+0.5
```
"""
function negativity(ρ::QuantumObject, subsys::Int; logarithmic::Bool = false)
- mask = fill(false, length(ρ.dims))
+ mask = fill(false, length(ρ.dimensions))
try
mask[subsys] = true
catch
@@ -62,43 +64,54 @@ end
Return the partial transpose of a density matrix ``\rho``, where `mask` is an array/vector with length that equals the length of `ρ.dims`. The elements in `mask` are boolean (`true` or `false`) which indicates whether or not the corresponding subsystem should be transposed.
# Arguments
-- `ρ::QuantumObject`: The density matrix (`ρ.type` must be [`OperatorQuantumObject`](@ref)).
+- `ρ::QuantumObject`: The density matrix (`ρ.type` must be [`Operator`](@ref)).
- `mask::Vector{Bool}`: A boolean vector selects which subsystems should be transposed.
# Returns
- `ρ_pt::QuantumObject`: The density matrix with the selected subsystems transposed.
"""
-function partial_transpose(ρ::QuantumObject{T,OperatorQuantumObject}, mask::Vector{Bool}) where {T}
- if length(mask) != length(ρ.dims)
+function partial_transpose(ρ::QuantumObject{Operator}, mask::Vector{Bool})
+ any(s -> s isa EnrSpace, ρ.dimensions.to) && throw(ArgumentError("partial_transpose does not support EnrSpace"))
+
+ (length(mask) != length(ρ.dimensions)) &&
throw(ArgumentError("The length of \`mask\` should be equal to the length of \`ρ.dims\`."))
- end
+
+ isa(ρ.dimensions, GeneralDimensions) &&
+ (get_dimensions_to(ρ) != get_dimensions_from(ρ)) &&
+ throw(ArgumentError("Invalid partial transpose for dims = $(_get_dims_string(ρ.dimensions))"))
+
return _partial_transpose(ρ, mask)
end
# for dense matrices
-function _partial_transpose(ρ::QuantumObject{<:AbstractArray,OperatorQuantumObject}, mask::Vector{Bool})
- mask2 = [1 + Int(i) for i in mask]
+function _partial_transpose(ρ::QuantumObject{Operator}, mask::Vector{Bool})
+ nsys = length(mask)
+ mask2 = reverse([mask[s] ? 2 : 1 for s in 1:nsys])
# mask2 has elements with values equal to 1 or 2
- # 1 - the subsystem don't need to be transposed
- # 2 - the subsystem need be transposed
+ # 1 - the subsystem (in reversed order) don't need to be transposed
+ # 2 - the subsystem (in reversed order) need to be transposed
- nsys = length(mask2)
+ dims_rev = reverse(dimensions_to_dims(get_dimensions_to(ρ)))
pt_dims = reshape(Vector(1:(2*nsys)), (nsys, 2))
pt_idx = [
- [pt_dims[n, mask2[n]] for n in 1:nsys] # origin value in mask2
- [pt_dims[n, 3-mask2[n]] for n in 1:nsys] # opposite value in mask2 (1 -> 2, and 2 -> 1)
+ [pt_dims[n, mask2[n]] for n in 1:nsys] # origin value in mask2
+ [pt_dims[n, 3-mask2[n]] for n in 1:nsys] # opposite value in mask2 (1 -> 2, and 2 -> 1)
]
return QuantumObject(
- reshape(permutedims(reshape(ρ.data, (ρ.dims..., ρ.dims...)), pt_idx), size(ρ)),
- Operator,
- ρ.dims,
+ reshape(permutedims(reshape(ρ.data, (dims_rev..., dims_rev...)), pt_idx), size(ρ)),
+ Operator(),
+ Dimensions(ρ.dimensions.to),
)
end
# for sparse matrices
-function _partial_transpose(ρ::QuantumObject{<:AbstractSparseArray,OperatorQuantumObject}, mask::Vector{Bool})
+function _partial_transpose(
+ ρ::QuantumObject{Operator,DimsType,<:AbstractSparseArray},
+ mask::Vector{Bool},
+) where {DimsType<:AbstractDimensions}
M, N = size(ρ)
- dimsTuple = Tuple(ρ.dims)
+ dims_rev = reverse(Tuple(dimensions_to_dims(get_dimensions_to(ρ))))
+ mask_rev = reverse(mask)
colptr = ρ.data.colptr
rowval = ρ.data.rowval
nzval = ρ.data.nzval
@@ -118,17 +131,17 @@ function _partial_transpose(ρ::QuantumObject{<:AbstractSparseArray,OperatorQuan
I_pt[n] = i
J_pt[n] = j
else
- ket_pt = [Base._ind2sub(dimsTuple, i)...]
- bra_pt = [Base._ind2sub(dimsTuple, j)...]
- for sys in findall(m -> m, mask)
+ ket_pt = [Base._ind2sub(dims_rev, i)...]
+ bra_pt = [Base._ind2sub(dims_rev, j)...]
+ for sys in findall(m -> m, mask_rev)
@inbounds ket_pt[sys], bra_pt[sys] = bra_pt[sys], ket_pt[sys]
end
- I_pt[n] = Base._sub2ind(dimsTuple, ket_pt...)
- J_pt[n] = Base._sub2ind(dimsTuple, bra_pt...)
+ I_pt[n] = Base._sub2ind(dims_rev, ket_pt...)
+ J_pt[n] = Base._sub2ind(dims_rev, bra_pt...)
end
V_pt[n] = nzval[p]
end
end
- return QuantumObject(sparse(I_pt, J_pt, V_pt, M, N), Operator, ρ.dims)
+ return QuantumObject(sparse(I_pt, J_pt, V_pt, M, N), Operator(), ρ.dimensions)
end
diff --git a/src/permutation.jl b/src/permutation.jl
deleted file mode 100644
index 32715790f..000000000
--- a/src/permutation.jl
+++ /dev/null
@@ -1,38 +0,0 @@
-export bdf, get_bdf_blocks
-
-function bdf(A::SparseMatrixCSC{T,M}) where {T,M}
- n = LinearAlgebra.checksquare(A)
-
- G = DiGraph(abs.(A) .> 0)
- idxs = connected_components(G)
- P = sparse(1:n, reduce(vcat, idxs), ones(n), n, n)
- block_sizes = map(length, idxs)
-
- return P, P * A * P', block_sizes
-end
-
-function bdf(
- A::QuantumObject{SparseMatrixCSC{T,M},OpType},
-) where {T,M,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
- P, A_bd, block_sizes = bdf(A.data)
- return P, QuantumObject(A_bd, A.type, A.dims), block_sizes
-end
-
-function get_bdf_blocks(A::SparseMatrixCSC{T,M}, block_sizes::Vector{Int}) where {T,M}
- num_blocks = length(block_sizes)
- block_indices = M[1]
- block_list = [A[1:block_sizes[1], 1:block_sizes[1]]]
- for i in 2:num_blocks
- idx = sum(view(block_sizes, 1:i-1)) + 1
- push!(block_indices, idx)
- push!(block_list, A[idx:idx-1+block_sizes[i], idx:idx-1+block_sizes[i]])
- end
- return block_list, block_indices
-end
-
-function get_bdf_blocks(
- A::QuantumObject{SparseMatrixCSC{T,M},OpType},
- block_sizes::Vector{Int},
-) where {T,M,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
- return get_bdf_blocks(A.data, block_sizes)
-end
diff --git a/src/progress_bar.jl b/src/progress_bar.jl
index 40c81d452..0fe989f52 100644
--- a/src/progress_bar.jl
+++ b/src/progress_bar.jl
@@ -1,6 +1,6 @@
export ProgressBar, next!
-struct ProgressBar{CT,T1<:Integer,T2<:Real,T3,T4<:Real,LT}
+mutable struct ProgressBar{CT,T1<:Integer,T2<:Real,T3,T4<:Real,LT}
counter::CT
max_counts::T1
enable::Bool
diff --git a/src/qobj/arithmetic_and_attributes.jl b/src/qobj/arithmetic_and_attributes.jl
index 4ba7d4bc2..1f6c245c1 100644
--- a/src/qobj/arithmetic_and_attributes.jl
+++ b/src/qobj/arithmetic_and_attributes.jl
@@ -4,7 +4,7 @@ Arithmetic and Attributes for QuantumObject
- export most of the attribute functions in "Python Qobj class"
=#
-export proj, ptrace, purity, permute
+export proj, ptrace, purity
export tidyup, tidyup!
export get_data, get_coherence
@@ -13,206 +13,221 @@ Base.broadcastable(x::QuantumObject) = x.data
for op in (:(+), :(-), :(*), :(/), :(^))
@eval begin
function Base.Broadcast.broadcasted(::typeof($op), x::QuantumObject, y::QuantumObject)
- return QuantumObject(broadcast($op, x.data, y.data), x.type, x.dims)
+ return QuantumObject(broadcast($op, x.data, y.data), x.type, x.dimensions)
end
function Base.Broadcast.broadcasted(::typeof($op), x::QuantumObject, y::Number)
- return QuantumObject(broadcast($op, x.data, y), x.type, x.dims)
+ return QuantumObject(broadcast($op, x.data, y), x.type, x.dimensions)
end
function Base.Broadcast.broadcasted(::typeof($op), x::Number, y::QuantumObject)
- return QuantumObject(broadcast($op, x, y.data), y.type, y.dims)
+ return QuantumObject(broadcast($op, x, y.data), y.type, y.dimensions)
end
function Base.Broadcast.broadcasted(::typeof($op), x::QuantumObject, y::AbstractArray)
- return QuantumObject(broadcast($op, x.data, y), x.type, x.dims)
+ return QuantumObject(broadcast($op, x.data, y), x.type, x.dimensions)
end
function Base.Broadcast.broadcasted(::typeof($op), x::AbstractArray, y::QuantumObject)
- return QuantumObject(broadcast($op, x, y.data), y.type, y.dims)
+ return QuantumObject(broadcast($op, x, y.data), y.type, y.dimensions)
end
end
end
for op in (:(+), :(-), :(*))
@eval begin
- function LinearAlgebra.$op(
- A::QuantumObject{<:AbstractArray{T1},OpType},
- B::QuantumObject{<:AbstractArray{T2},OpType},
- ) where {T1,T2,OpType<:QuantumObjectType}
- A.dims != B.dims &&
- throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
- return QuantumObject($(op)(A.data, B.data), A.type, A.dims)
+ function Base.$op(A::AbstractQuantumObject, B::AbstractQuantumObject)
+ check_dimensions(A, B)
+ QType = promote_op_type(A, B)
+ return QType($(op)(A.data, B.data), A.type, A.dimensions)
end
- LinearAlgebra.$op(A::QuantumObject{<:AbstractArray{T}}) where {T} = QuantumObject($(op)(A.data), A.type, A.dims)
+ Base.$op(A::AbstractQuantumObject) = get_typename_wrapper(A)($(op)(A.data), A.type, A.dimensions)
- LinearAlgebra.$op(n::T1, A::QuantumObject{<:AbstractArray{T2}}) where {T1<:Number,T2} =
- QuantumObject($(op)(n * I, A.data), A.type, A.dims)
- LinearAlgebra.$op(A::QuantumObject{<:AbstractArray{T1}}, n::T2) where {T1,T2<:Number} =
- QuantumObject($(op)(A.data, n * I), A.type, A.dims)
+ Base.$op(n::T, A::AbstractQuantumObject) where {T<:Number} =
+ get_typename_wrapper(A)($(op)(n * I, A.data), A.type, A.dimensions)
+ Base.$op(A::AbstractQuantumObject, n::T) where {T<:Number} =
+ get_typename_wrapper(A)($(op)(A.data, n * I), A.type, A.dimensions)
end
end
-function LinearAlgebra.:(*)(
- A::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
- return QuantumObject(A.data * B.data, Ket, A.dims)
+function check_mul_dimensions(from::NTuple{NA,AbstractSpace}, to::NTuple{NB,AbstractSpace}) where {NA,NB}
+ (from != to) && throw(
+ DimensionMismatch(
+ "The quantum object with (right) dims = $(dimensions_to_dims(from)) can not multiply a quantum object with (left) dims = $(dimensions_to_dims(to)) on the right-hand side.",
+ ),
+ )
+ return nothing
+end
+
+for ADimType in (:Dimensions, :GeneralDimensions)
+ for BDimType in (:Dimensions, :GeneralDimensions)
+ if ADimType == BDimType == :Dimensions
+ @eval begin
+ function Base.:(*)(
+ A::AbstractQuantumObject{Operator,<:$ADimType},
+ B::AbstractQuantumObject{Operator,<:$BDimType},
+ )
+ check_dimensions(A, B)
+ QType = promote_op_type(A, B)
+ return QType(A.data * B.data, Operator(), A.dimensions)
+ end
+ end
+ else
+ @eval begin
+ function Base.:(*)(
+ A::AbstractQuantumObject{Operator,<:$ADimType},
+ B::AbstractQuantumObject{Operator,<:$BDimType},
+ )
+ check_mul_dimensions(get_dimensions_from(A), get_dimensions_to(B))
+ QType = promote_op_type(A, B)
+ return QType(
+ A.data * B.data,
+ Operator(),
+ GeneralDimensions(get_dimensions_to(A), get_dimensions_from(B)),
+ )
+ end
+ end
+ end
+ end
+end
+
+function Base.:(*)(A::AbstractQuantumObject{Operator}, B::QuantumObject{Ket,<:Dimensions})
+ check_mul_dimensions(get_dimensions_from(A), get_dimensions_to(B))
+ return QuantumObject(A.data * B.data, Ket(), Dimensions(get_dimensions_to(A)))
end
-function LinearAlgebra.:(*)(
- A::QuantumObject{<:AbstractArray{T1},BraQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
- return QuantumObject(A.data * B.data, Bra, A.dims)
+function Base.:(*)(A::QuantumObject{Bra,<:Dimensions}, B::AbstractQuantumObject{Operator})
+ check_mul_dimensions(get_dimensions_from(A), get_dimensions_to(B))
+ return QuantumObject(A.data * B.data, Bra(), Dimensions(get_dimensions_from(B)))
end
-function LinearAlgebra.:(*)(
- A::QuantumObject{<:AbstractArray{T1},KetQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},BraQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
- return QuantumObject(A.data * B.data, Operator, A.dims)
+function Base.:(*)(A::QuantumObject{Ket}, B::QuantumObject{Bra})
+ check_dimensions(A, B)
+ return QuantumObject(A.data * B.data, Operator(), A.dimensions) # to align with QuTiP, don't use kron(A, B) to do it.
end
-function LinearAlgebra.:(*)(
- A::QuantumObject{<:AbstractArray{T1},BraQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
+function Base.:(*)(A::QuantumObject{Bra}, B::QuantumObject{Ket})
+ check_dimensions(A, B)
return A.data * B.data
end
-function LinearAlgebra.:(*)(
- A::QuantumObject{<:AbstractArray{T1},SuperOperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
- return QuantumObject(vec2mat(A.data * mat2vec(B.data)), Operator, A.dims)
+function Base.:(*)(A::AbstractQuantumObject{SuperOperator}, B::QuantumObject{Operator})
+ check_dimensions(A, B)
+ return QuantumObject(vec2mat(A.data * mat2vec(B.data)), Operator(), A.dimensions)
end
-function LinearAlgebra.:(*)(
- A::QuantumObject{<:AbstractArray{T1},OperatorBraQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},OperatorKetQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
+function Base.:(*)(A::QuantumObject{OperatorBra}, B::QuantumObject{OperatorKet})
+ check_dimensions(A, B)
return A.data * B.data
end
-function LinearAlgebra.:(*)(
- A::QuantumObject{<:AbstractArray{T1},SuperOperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},OperatorKetQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
- return QuantumObject(A.data * B.data, OperatorKet, A.dims)
+function Base.:(*)(A::AbstractQuantumObject{SuperOperator}, B::QuantumObject{OperatorKet})
+ check_dimensions(A, B)
+ return QuantumObject(A.data * B.data, OperatorKet(), A.dimensions)
end
-function LinearAlgebra.:(*)(
- A::QuantumObject{<:AbstractArray{T1},OperatorBraQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},SuperOperatorQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
- return QuantumObject(A.data * B.data, OperatorBra, A.dims)
+function Base.:(*)(A::QuantumObject{OperatorBra}, B::AbstractQuantumObject{SuperOperator})
+ check_dimensions(A, B)
+ return QuantumObject(A.data * B.data, OperatorBra(), A.dimensions)
end
-LinearAlgebra.:(^)(A::QuantumObject{<:AbstractArray{T}}, n::T1) where {T,T1<:Number} =
- QuantumObject(^(A.data, n), A.type, A.dims)
-LinearAlgebra.:(/)(A::QuantumObject{<:AbstractArray{T}}, n::T1) where {T,T1<:Number} =
- QuantumObject(/(A.data, n), A.type, A.dims)
+Base.:(^)(A::QuantumObject, n::T) where {T<:Number} = QuantumObject(^(A.data, n), A.type, A.dimensions)
+Base.:(/)(A::AbstractQuantumObject, n::T) where {T<:Number} = get_typename_wrapper(A)(A.data / n, A.type, A.dimensions)
@doc raw"""
+ A ⋅ B
dot(A::QuantumObject, B::QuantumObject)
Compute the dot product between two [`QuantumObject`](@ref): ``\langle A | B \rangle``
Note that `A` and `B` should be [`Ket`](@ref) or [`OperatorKet`](@ref)
-`A ⋅ B` (where `⋅` can be typed by tab-completing `\cdot` in the REPL) is a synonym for `dot(A, B)`
+!!! note
+ `A ⋅ B` (where `⋅` can be typed by tab-completing `\cdot` in the REPL) is a synonym of `dot(A, B)`.
"""
-function LinearAlgebra.dot(
- A::QuantumObject{<:AbstractArray{T1},OpType},
- B::QuantumObject{<:AbstractArray{T2},OpType},
-) where {T1<:Number,T2<:Number,OpType<:Union{KetQuantumObject,OperatorKetQuantumObject}}
- A.dims != B.dims && throw(DimensionMismatch("The quantum objects are not of the same Hilbert dimension."))
+function LinearAlgebra.dot(A::QuantumObject{OpType}, B::QuantumObject{OpType}) where {OpType<:Union{Ket,OperatorKet}}
+ check_dimensions(A, B)
return LinearAlgebra.dot(A.data, B.data)
end
@doc raw"""
- dot(i::QuantumObject, A::QuantumObject j::QuantumObject)
+ dot(i::QuantumObject, A::AbstractQuantumObject j::QuantumObject)
+ matrix_element(i::QuantumObject, A::AbstractQuantumObject 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 a [`AbstractQuantumObject`](@ref) and two [`QuantumObject`](@ref) (`i` and `j`), namely ``\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).
- `A` is in the type of [`SuperOperator`](@ref), with `i` and `j` are both [`OperatorKet`](@ref)
+
+!!! note
+ `matrix_element(i, A, j)` is a synonym of `dot(i, A, j)`.
"""
-function LinearAlgebra.dot(
- i::QuantumObject{<:AbstractArray{T1},KetQuantumObject},
- A::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
- j::QuantumObject{<:AbstractArray{T3},KetQuantumObject},
-) where {T1<:Number,T2<:Number,T3<:Number}
- ((i.dims != A.dims) || (A.dims != j.dims)) &&
- throw(DimensionMismatch("The quantum objects are not of the same Hilbert dimension."))
+function LinearAlgebra.dot(i::QuantumObject{Ket}, A::AbstractQuantumObject{Operator}, j::QuantumObject{Ket})
+ check_dimensions(i, A, j)
return LinearAlgebra.dot(i.data, A.data, j.data)
end
function LinearAlgebra.dot(
- i::QuantumObject{<:AbstractArray{T1},OperatorKetQuantumObject},
- A::QuantumObject{<:AbstractArray{T2},SuperOperatorQuantumObject},
- j::QuantumObject{<:AbstractArray{T3},OperatorKetQuantumObject},
-) where {T1<:Number,T2<:Number,T3<:Number}
- ((i.dims != A.dims) || (A.dims != j.dims)) &&
- throw(DimensionMismatch("The quantum objects are not of the same Hilbert dimension."))
+ i::QuantumObject{OperatorKet},
+ A::AbstractQuantumObject{SuperOperator},
+ j::QuantumObject{OperatorKet},
+)
+ check_dimensions(i, A, j)
return LinearAlgebra.dot(i.data, A.data, j.data)
end
@doc raw"""
- conj(A::QuantumObject)
+ zero(A::AbstractQuantumObject)
+
+Return a similar [`AbstractQuantumObject`](@ref) with `dims` and `type` are same as `A`, but `data` is a zero-array.
+"""
+Base.zero(A::AbstractQuantumObject) = get_typename_wrapper(A)(zero(A.data), A.type, A.dimensions)
+
+@doc raw"""
+ one(A::AbstractQuantumObject)
+
+Return a similar [`AbstractQuantumObject`](@ref) with `dims` and `type` are same as `A`, but `data` is an identity matrix.
+
+Note that `A` must be [`Operator`](@ref) or [`SuperOperator`](@ref).
+"""
+Base.one(A::AbstractQuantumObject{OpType}) where {OpType<:Union{Operator,SuperOperator}} =
+ get_typename_wrapper(A)(one(A.data), A.type, A.dimensions)
+
+@doc raw"""
+ conj(A::AbstractQuantumObject)
-Return the element-wise complex conjugation of the [`QuantumObject`](@ref).
+Return the element-wise complex conjugation of the [`AbstractQuantumObject`](@ref).
"""
-Base.conj(A::QuantumObject{<:AbstractArray{T}}) where {T} = QuantumObject(conj(A.data), A.type, A.dims)
+Base.conj(A::AbstractQuantumObject) = get_typename_wrapper(A)(conj(A.data), A.type, A.dimensions)
@doc raw"""
- transpose(A::QuantumObject)
+ transpose(A::AbstractQuantumObject)
-Lazy matrix transpose of the [`QuantumObject`](@ref).
+Lazy matrix transpose of the [`AbstractQuantumObject`](@ref).
"""
-LinearAlgebra.transpose(
- A::QuantumObject{<:AbstractArray{T},OpType},
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(transpose(A.data), A.type, A.dims)
+Base.transpose(A::AbstractQuantumObject{OpType}) where {OpType<:Union{Operator,SuperOperator}} =
+ get_typename_wrapper(A)(transpose(A.data), A.type, transpose(A.dimensions))
@doc raw"""
A'
- adjoint(A::QuantumObject)
+ adjoint(A::AbstractQuantumObject)
+ dag(A::AbstractQuantumObject)
-Lazy adjoint (conjugate transposition) of the [`QuantumObject`](@ref)
+Lazy adjoint (conjugate transposition) of the [`AbstractQuantumObject`](@ref)
-Note that `A'` is a synonym for `adjoint(A)`
+!!! note
+ `A'` and `dag(A)` are synonyms of `adjoint(A)`.
"""
-LinearAlgebra.adjoint(
- A::QuantumObject{<:AbstractArray{T},OpType},
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(adjoint(A.data), A.type, A.dims)
-LinearAlgebra.adjoint(A::QuantumObject{<:AbstractArray{T},KetQuantumObject}) where {T} =
- QuantumObject(adjoint(A.data), Bra, A.dims)
-LinearAlgebra.adjoint(A::QuantumObject{<:AbstractArray{T},BraQuantumObject}) where {T} =
- QuantumObject(adjoint(A.data), Ket, A.dims)
-LinearAlgebra.adjoint(A::QuantumObject{<:AbstractArray{T},OperatorKetQuantumObject}) where {T} =
- QuantumObject(adjoint(A.data), OperatorBra, A.dims)
-LinearAlgebra.adjoint(A::QuantumObject{<:AbstractArray{T},OperatorBraQuantumObject}) where {T} =
- QuantumObject(adjoint(A.data), OperatorKet, A.dims)
+Base.adjoint(A::AbstractQuantumObject{OpType}) where {OpType<:Union{Operator,SuperOperator}} =
+ get_typename_wrapper(A)(adjoint(A.data), A.type, adjoint(A.dimensions))
+Base.adjoint(A::QuantumObject{Ket}) = QuantumObject(adjoint(A.data), Bra(), adjoint(A.dimensions))
+Base.adjoint(A::QuantumObject{Bra}) = QuantumObject(adjoint(A.data), Ket(), adjoint(A.dimensions))
+Base.adjoint(A::QuantumObject{OperatorKet}) = QuantumObject(adjoint(A.data), OperatorBra(), adjoint(A.dimensions))
+Base.adjoint(A::QuantumObject{OperatorBra}) = QuantumObject(adjoint(A.data), OperatorKet(), adjoint(A.dimensions))
@doc raw"""
- inv(A::QuantumObject)
+ inv(A::AbstractQuantumObject)
-Matrix inverse of the [`QuantumObject`](@ref)
+Matrix inverse of the [`AbstractQuantumObject`](@ref). If `A` is a [`QuantumObjectEvolution`](@ref), the inverse is computed at the last computed time.
"""
-LinearAlgebra.inv(
- A::QuantumObject{<:AbstractArray{T},OpType},
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(sparse(inv(Matrix(A.data))), A.type, A.dims)
+LinearAlgebra.inv(A::AbstractQuantumObject{OpType}) where {OpType<:Union{Operator,SuperOperator}} =
+ QuantumObject(sparse(inv(Matrix(A.data))), A.type, A.dimensions)
-LinearAlgebra.Hermitian(
- A::QuantumObject{<:AbstractArray{T},OpType},
- uplo::Symbol = :U,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(Hermitian(A.data, uplo), A.type, A.dims)
+LinearAlgebra.Hermitian(A::QuantumObject{OpType}, uplo::Symbol = :U) where {OpType<:Union{Operator,SuperOperator}} =
+ QuantumObject(Hermitian(A.data, uplo), A.type, A.dimensions)
@doc raw"""
tr(A::QuantumObject)
@@ -223,35 +238,32 @@ Note that this function only supports for [`Operator`](@ref) and [`SuperOperator
# Examples
-```
+```jldoctest
julia> a = destroy(20)
-Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false
+
+Quantum Object: type=Operator() dims=[20] size=(20, 20) ishermitian=false
20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries:
-⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢
+⎡⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⎦
julia> tr(a' * a)
190.0 + 0.0im
```
"""
+LinearAlgebra.tr(A::QuantumObject{OpType}) where {OpType<:Union{Operator,SuperOperator}} = tr(A.data)
LinearAlgebra.tr(
- A::QuantumObject{<:AbstractArray{T},OpType},
-) 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))
+ A::QuantumObject{OpType,DimsType,<:Union{<:Hermitian{TF},Symmetric{TR}}},
+) where {OpType<:Operator,DimsType,TF<:BlasFloat,TR<:Real} = real(tr(A.data))
@doc raw"""
svdvals(A::QuantumObject)
Return the singular values of a [`QuantumObject`](@ref) in descending order
"""
-LinearAlgebra.svdvals(A::QuantumObject{<:AbstractVector}) = svdvals(A.data)
-LinearAlgebra.svdvals(A::QuantumObject{<:AbstractMatrix}) = svdvals(A.data)
-LinearAlgebra.svdvals(A::QuantumObject{<:AbstractSparseMatrix}) = svdvals(sparse_to_dense(A.data))
+LinearAlgebra.svdvals(A::QuantumObject) = svdvals(to_dense(A.data))
@doc raw"""
norm(A::QuantumObject, p::Real)
@@ -263,9 +275,10 @@ Return the standard vector `p`-norm or [Schatten](https://en.wikipedia.org/wiki/
# Examples
-```
+```jldoctest
julia> ψ = fock(10, 2)
-Quantum Object: type=Ket dims=[10] size=(10,)
+
+Quantum Object: type=Ket() dims=[10] size=(10,)
10-element Vector{ComplexF64}:
0.0 + 0.0im
0.0 + 0.0im
@@ -282,21 +295,16 @@ julia> norm(ψ)
1.0
```
"""
-LinearAlgebra.norm(
- A::QuantumObject{<:AbstractArray{T},OpType},
- p::Real = 2,
-) where {T,OpType<:Union{KetQuantumObject,BraQuantumObject,OperatorKetQuantumObject,OperatorBraQuantumObject}} =
+LinearAlgebra.norm(A::QuantumObject{OpType}, p::Real = 2) where {OpType<:Union{Ket,Bra,OperatorKet,OperatorBra}} =
norm(A.data, p)
-function LinearAlgebra.norm(
- A::QuantumObject{<:AbstractArray{T},OpType},
- p::Real = 1,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
+function LinearAlgebra.norm(A::QuantumObject{OpType}, p::Real = 1) where {OpType<:Union{Operator,SuperOperator}}
p == 2.0 && return norm(A.data, 2)
return norm(svdvals(A), p)
end
@doc raw"""
normalize(A::QuantumObject, p::Real)
+ unit(A::QuantumObject, p::Real)
Return normalized [`QuantumObject`](@ref) so that its `p`-norm equals to unity, i.e. `norm(A, p) == 1`.
@@ -304,14 +312,15 @@ Support for the following types of [`QuantumObject`](@ref):
- If `A` is [`Ket`](@ref) or [`Bra`](@ref), default `p = 2`
- If `A` is [`Operator`](@ref), default `p = 1`
+!!! note
+ `unit` is a synonym of `normalize`.
+
Also, see [`norm`](@ref) about its definition for different types of [`QuantumObject`](@ref).
"""
-LinearAlgebra.normalize(
- A::QuantumObject{<:AbstractArray{T},ObjType},
- p::Real = 2,
-) where {T,ObjType<:Union{KetQuantumObject,BraQuantumObject}} = QuantumObject(A.data / norm(A, p), A.type, A.dims)
-LinearAlgebra.normalize(A::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}, p::Real = 1) where {T} =
- QuantumObject(A.data / norm(A, p), A.type, A.dims)
+LinearAlgebra.normalize(A::QuantumObject{ObjType}, p::Real = 2) where {ObjType<:Union{Ket,Bra}} =
+ QuantumObject(A.data / norm(A, p), A.type, A.dimensions)
+LinearAlgebra.normalize(A::QuantumObject{Operator}, p::Real = 1) =
+ QuantumObject(A.data / norm(A, p), A.type, A.dimensions)
@doc raw"""
normalize!(A::QuantumObject, p::Real)
@@ -324,37 +333,23 @@ Support for the following types of [`QuantumObject`](@ref):
Also, see [`norm`](@ref) about its definition for different types of [`QuantumObject`](@ref).
"""
-LinearAlgebra.normalize!(
- A::QuantumObject{<:AbstractArray{T},ObjType},
- p::Real = 2,
-) where {T,ObjType<:Union{KetQuantumObject,BraQuantumObject}} = (rmul!(A.data, 1 / norm(A, p)); A)
-LinearAlgebra.normalize!(A::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}, p::Real = 1) where {T} =
+LinearAlgebra.normalize!(A::QuantumObject{ObjType}, p::Real = 2) where {ObjType<:Union{Ket,Bra}} =
(rmul!(A.data, 1 / norm(A, p)); A)
+LinearAlgebra.normalize!(A::QuantumObject{Operator}, p::Real = 1) = (rmul!(A.data, 1 / norm(A, p)); A)
+
+LinearAlgebra.triu!(A::QuantumObject{OpType}, k::Integer = 0) where {OpType<:Union{Operator,SuperOperator}} =
+ (triu!(A.data, k); A)
+LinearAlgebra.tril!(A::QuantumObject{OpType}, k::Integer = 0) where {OpType<:Union{Operator,SuperOperator}} =
+ (tril!(A.data, k); A)
+LinearAlgebra.triu(A::QuantumObject{OpType}, k::Integer = 0) where {OpType<:Union{Operator,SuperOperator}} =
+ QuantumObject(triu(A.data, k), A.type, A.dimensions)
+LinearAlgebra.tril(A::QuantumObject{OpType}, k::Integer = 0) where {OpType<:Union{Operator,SuperOperator}} =
+ QuantumObject(tril(A.data, k), A.type, A.dimensions)
+
+LinearAlgebra.lmul!(a::Number, B::QuantumObject) = (lmul!(a, B.data); B)
+LinearAlgebra.rmul!(B::QuantumObject, a::Number) = (rmul!(B.data, a); B)
-LinearAlgebra.triu!(
- A::QuantumObject{<:AbstractArray{T},OpType},
- k::Integer = 0,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = (triu!(A.data, k); A)
-LinearAlgebra.tril!(
- A::QuantumObject{<:AbstractArray{T},OpType},
- k::Integer = 0,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = (tril!(A.data, k); A)
-LinearAlgebra.triu(
- A::QuantumObject{<:AbstractArray{T},OpType},
- k::Integer = 0,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(triu(A.data, k), A.type, A.dims)
-LinearAlgebra.tril(
- A::QuantumObject{<:AbstractArray{T},OpType},
- k::Integer = 0,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(tril(A.data, k), A.type, A.dims)
-
-LinearAlgebra.lmul!(a::Number, B::QuantumObject{<:AbstractArray}) = (lmul!(a, B.data); B)
-LinearAlgebra.rmul!(B::QuantumObject{<:AbstractArray}, a::Number) = (rmul!(B.data, a); B)
-
-@inline LinearAlgebra.mul!(y::AbstractVector{Ty}, A::QuantumObject{<:AbstractMatrix{Ta}}, x, α, β) where {Ty,Ta} =
- mul!(y, A.data, x, α, β)
+@inline LinearAlgebra.mul!(y::AbstractVector{T}, A::QuantumObject, x, α, β) where {T} = mul!(y, A.data, x, α, β)
@doc raw"""
√(A)
@@ -362,10 +357,10 @@ LinearAlgebra.rmul!(B::QuantumObject{<:AbstractArray}, a::Number) = (rmul!(B.dat
Matrix square root of [`QuantumObject`](@ref)
-Note that `√(A)` is a synonym for `sqrt(A)`
+!!! note
+ `√(A)` (where `√` can be typed by tab-completing `\sqrt` in the REPL) is a synonym of `sqrt(A)`.
"""
-LinearAlgebra.sqrt(A::QuantumObject{<:AbstractArray{T}}) where {T} =
- QuantumObject(sqrt(sparse_to_dense(A.data)), A.type, A.dims)
+Base.sqrt(A::QuantumObject) = QuantumObject(sqrt(to_dense(A.data)), A.type, A.dimensions)
@doc raw"""
log(A::QuantumObject)
@@ -374,10 +369,8 @@ Matrix logarithm of [`QuantumObject`](@ref)
Note that this function only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
"""
-LinearAlgebra.log(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(log(sparse_to_dense(A.data)), A.type, A.dims)
+Base.log(A::QuantumObject{ObjType}) where {ObjType<:Union{Operator,SuperOperator}} =
+ QuantumObject(log(to_dense(A.data)), A.type, A.dimensions)
@doc raw"""
exp(A::QuantumObject)
@@ -386,16 +379,13 @@ Matrix exponential of [`QuantumObject`](@ref)
Note that this function only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
"""
-LinearAlgebra.exp(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(dense_to_sparse(exp(A.data)), A.type, A.dims)
-LinearAlgebra.exp(
- A::QuantumObject{<:AbstractSparseMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(_spexp(A.data), A.type, A.dims)
-
-function _spexp(A::SparseMatrixCSC{T,M}; threshold = 1e-14, nonzero_tol = 1e-20) where {T,M}
+Base.exp(A::QuantumObject{ObjType,DimsType,<:AbstractMatrix}) where {ObjType<:Union{Operator,SuperOperator},DimsType} =
+ QuantumObject(to_sparse(exp(A.data)), A.type, A.dimensions)
+Base.exp(
+ A::QuantumObject{ObjType,DimsType,<:AbstractSparseMatrix},
+) where {ObjType<:Union{Operator,SuperOperator},DimsType} = QuantumObject(_spexp(A.data), A.type, A.dimensions)
+
+function _spexp(A::SparseMatrixCSC{T,M}; threshold = 1e-14, nonzero_tol = 1e-20) where {T<:Number,M<:Int}
m = checksquare(A) # Throws exception if not square
mat_norm = norm(A, Inf)
@@ -435,9 +425,8 @@ Matrix sine of [`QuantumObject`](@ref), defined as
Note that this function only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
"""
-LinearAlgebra.sin(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = (exp(1im * A) - exp(-1im * A)) / 2im
+Base.sin(A::QuantumObject{ObjType}) where {ObjType<:Union{Operator,SuperOperator}} =
+ (exp(1im * A) - exp(-1im * A)) / 2im
@doc raw"""
cos(A::QuantumObject)
@@ -448,9 +437,7 @@ Matrix cosine of [`QuantumObject`](@ref), defined as
Note that this function only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
"""
-LinearAlgebra.cos(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = (exp(1im * A) + exp(-1im * A)) / 2
+Base.cos(A::QuantumObject{ObjType}) where {ObjType<:Union{Operator,SuperOperator}} = (exp(1im * A) + exp(-1im * A)) / 2
@doc raw"""
diag(A::QuantumObject, k::Int=0)
@@ -459,30 +446,31 @@ Return the `k`-th diagonal elements of a matrix-type [`QuantumObject`](@ref)
Note that this function only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
"""
-LinearAlgebra.diag(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
- k::Int = 0,
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = diag(A.data, k)
+LinearAlgebra.diag(A::QuantumObject{ObjType}, k::Int = 0) where {ObjType<:Union{Operator,SuperOperator}} =
+ diag(A.data, k)
@doc raw"""
proj(ψ::QuantumObject)
Return the projector for a [`Ket`](@ref) or [`Bra`](@ref) type of [`QuantumObject`](@ref)
"""
-proj(ψ::QuantumObject{<:AbstractArray{T},KetQuantumObject}) where {T} = ψ * ψ'
-proj(ψ::QuantumObject{<:AbstractArray{T},BraQuantumObject}) where {T} = ψ' * ψ
+proj(ψ::QuantumObject{Ket}) = ψ * ψ'
+proj(ψ::QuantumObject{Bra}) = ψ' * ψ
@doc raw"""
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}``:
-```
+```jldoctest
julia> ψ = kron(fock(2,0), fock(2,1))
-Quantum Object: type=Ket dims=[2, 2] size=(4,)
+
+Quantum Object: type=Ket() dims=[2, 2] size=(4,)
4-element Vector{ComplexF64}:
0.0 + 0.0im
1.0 + 0.0im
@@ -490,16 +478,18 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,)
0.0 + 0.0im
julia> ptrace(ψ, 2)
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
+
+Quantum Object: type=Operator() dims=[2] size=(2, 2) ishermitian=true
2×2 Matrix{ComplexF64}:
0.0+0.0im 0.0+0.0im
0.0+0.0im 1.0+0.0im
```
or in an entangled state ``\ket{\psi} = \frac{1}{\sqrt{2}} \left( \ket{e,e} + \ket{g,g} \right)``:
-```
+```jldoctest
julia> ψ = 1 / √2 * (kron(fock(2,0), fock(2,0)) + kron(fock(2,1), fock(2,1)))
-Quantum Object: type=Ket dims=[2, 2] size=(4,)
+
+Quantum Object: type=Ket() dims=[2, 2] size=(4,)
4-element Vector{ComplexF64}:
0.7071067811865475 + 0.0im
0.0 + 0.0im
@@ -507,48 +497,88 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,)
0.7071067811865475 + 0.0im
julia> ptrace(ψ, 1)
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
+
+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,KetQuantumObject}, sel::Union{AbstractVector{Int},Tuple})
- length(QO.dims) == 1 && return QO
+function ptrace(QO::QuantumObject{Ket}, sel::Union{AbstractVector{Int},Tuple})
+ any(s -> s isa EnrSpace, QO.dimensions.to) && throw(ArgumentError("ptrace does not support EnrSpace"))
+
+ _non_static_array_warning("sel", sel)
+
+ if length(sel) == 0 # return full trace for empty sel
+ return tr(ket2dm(QO))
+ else
+ n_d = length(QO.dimensions)
+
+ (any(>(n_d), sel) || any(<(1), sel)) && throw(
+ ArgumentError("Invalid indices in `sel`: $(sel), the given QuantumObject only have $(n_d) sub-systems"),
+ )
+ allunique(sel) || throw(ArgumentError("Duplicate selection indices in `sel`: $(sel)"))
+ (n_d == 1) && return ket2dm(QO) # ptrace should always return Operator
+ end
- ρtr, dkeep = _ptrace_ket(QO.data, QO.dims, SVector(sel))
- return QuantumObject(ρtr, type = Operator, 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 = Dimensions(dkeep))
end
-ptrace(QO::QuantumObject{<:AbstractArray,BraQuantumObject}, sel::Union{AbstractVector{Int},Tuple}) = ptrace(QO', sel)
+ptrace(QO::QuantumObject{Bra}, sel::Union{AbstractVector{Int},Tuple}) = ptrace(QO', sel)
+
+function ptrace(QO::QuantumObject{Operator}, sel::Union{AbstractVector{Int},Tuple})
+ any(s -> s isa EnrSpace, QO.dimensions.to) && throw(ArgumentError("ptrace does not support EnrSpace"))
+
+ # TODO: support for special cases when some of the subsystems have same `to` and `from` space
+ isa(QO.dimensions, GeneralDimensions) &&
+ (get_dimensions_to(QO) != get_dimensions_from(QO)) &&
+ throw(ArgumentError("Invalid partial trace for dims = $(_get_dims_string(QO.dimensions))"))
+
+ _non_static_array_warning("sel", sel)
-function ptrace(QO::QuantumObject{<:AbstractArray,OperatorQuantumObject}, sel::Union{AbstractVector{Int},Tuple})
- length(QO.dims) == 1 && return QO
+ if length(sel) == 0 # return full trace for empty sel
+ return tr(QO)
+ else
+ n_d = length(QO.dimensions)
- ρtr, dkeep = _ptrace_oper(QO.data, QO.dims, SVector(sel))
- return QuantumObject(ρtr, type = Operator, dims = dkeep)
+ (any(>(n_d), sel) || any(<(1), sel)) && throw(
+ ArgumentError("Invalid indices in `sel`: $(sel), the given QuantumObject only have $(n_d) sub-systems"),
+ )
+ allunique(sel) || throw(ArgumentError("Duplicate selection indices in `sel`: $(sel)"))
+ (n_d == 1) && return QO
+ end
+
+ dims = dimensions_to_dims(get_dimensions_to(QO))
+ _sort_sel = sort(SVector{length(sel),Int}(sel))
+ ρtr, dkeep = _ptrace_oper(QO.data, dims, _sort_sel)
+ return QuantumObject(ρtr, type = Operator(), dims = Dimensions(dkeep))
end
ptrace(QO::QuantumObject, sel::Int) = ptrace(QO, SVector(sel))
function _ptrace_ket(QO::AbstractArray, dims::Union{SVector,MVector}, sel)
- nd = length(dims)
+ n_d = length(dims)
- nd == 1 && return QO, dims
+ n_d == 1 && return QO, dims
- qtrace = filter(i -> i ∉ sel, 1:nd)
+ qtrace = filter(i -> i ∉ sel, 1:n_d)
dkeep = dims[sel]
dtrace = dims[qtrace]
- # Concatenate sel and qtrace without loosing the length information
- sel_qtrace = ntuple(Val(nd)) do i
- if i <= length(sel)
- @inbounds sel[i]
+ n_t = length(dtrace)
+
+ # Concatenate qtrace and sel without losing the length information
+ # Tuple(qtrace..., sel...)
+ qtrace_sel = ntuple(Val(n_d)) do i
+ if i <= n_t
+ @inbounds qtrace[i]
else
- @inbounds qtrace[i-length(sel)]
+ @inbounds sel[i-n_t]
end
end
vmat = reshape(QO, reverse(dims)...)
- topermute = nd + 1 .- sel_qtrace
+ topermute = reverse(n_d + 1 .- qtrace_sel)
vmat = permutedims(vmat, topermute) # TODO: use PermutedDimsArray when Julia v1.11.0 is released
vmat = reshape(vmat, prod(dkeep), prod(dtrace))
@@ -556,40 +586,42 @@ function _ptrace_ket(QO::AbstractArray, dims::Union{SVector,MVector}, sel)
end
function _ptrace_oper(QO::AbstractArray, dims::Union{SVector,MVector}, sel)
- nd = length(dims)
+ n_d = length(dims)
- nd == 1 && return QO, dims
+ n_d == 1 && return QO, dims
- qtrace = filter(i -> i ∉ sel, 1:nd)
+ qtrace = filter(i -> i ∉ sel, 1:n_d)
dkeep = dims[sel]
dtrace = dims[qtrace]
- # Concatenate sel and qtrace without loosing the length information
- qtrace_sel = ntuple(Val(2 * nd)) do i
- if i <= length(qtrace)
+ n_k = length(dkeep)
+ n_t = length(dtrace)
+ _2_n_t = 2 * n_t
+
+ # Concatenate qtrace and sel without losing the length information
+ # Tuple(qtrace..., sel...)
+ qtrace_sel = ntuple(Val(2 * n_d)) do i
+ if i <= n_t
@inbounds qtrace[i]
- elseif i <= 2 * length(qtrace)
- @inbounds qtrace[i-length(qtrace)] + nd
- elseif i <= 2 * length(qtrace) + length(sel)
- @inbounds sel[i-length(qtrace)-length(sel)]
+ elseif i <= _2_n_t
+ @inbounds qtrace[i-n_t] + n_d
+ elseif i <= _2_n_t + n_k
+ @inbounds sel[i-_2_n_t]
else
- @inbounds sel[i-length(qtrace)-2*length(sel)] + nd
+ @inbounds sel[i-_2_n_t-n_k] + n_d
end
end
ρmat = reshape(QO, reverse(vcat(dims, dims))...)
- topermute = 2 * nd + 1 .- qtrace_sel
- ρmat = permutedims(ρmat, reverse(topermute)) # TODO: use PermutedDimsArray when Julia v1.11.0 is released
-
- ## TODO: Check if it works always
-
- # ρmat = row_major_reshape(ρmat, prod(dtrace), prod(dtrace), prod(dkeep), prod(dkeep))
- # res = dropdims(mapslices(tr, ρmat, dims=(1,2)), dims=(1,2))
+ topermute = reverse(2 * n_d + 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 = map(tr, eachslice(ρmat, dims = (1, 2)))
+ res = _map_trace(ρmat)
return res, dkeep
end
+_map_trace(A::AbstractArray{T,4}) where {T} = map(tr, eachslice(A, dims = (1, 2)))
+
@doc raw"""
purity(ρ::QuantumObject)
@@ -597,59 +629,58 @@ Calculate the purity of a [`QuantumObject`](@ref): ``\textrm{Tr}(\rho^2)``
Note that this function only supports for [`Ket`](@ref), [`Bra`](@ref), and [`Operator`](@ref)
"""
-purity(ρ::QuantumObject{<:AbstractArray{T},ObjType}) where {T,ObjType<:Union{KetQuantumObject,BraQuantumObject}} =
- sum(abs2, ρ.data)
-purity(ρ::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} = real(tr(ρ.data^2))
+purity(ρ::QuantumObject{ObjType}) where {ObjType<:Union{Ket,Bra}} = sum(abs2, ρ.data)
+purity(ρ::QuantumObject{Operator}) = real(tr(ρ.data^2))
@doc raw"""
- tidyup(A::QuantumObject, tol::Real=1e-14)
+ tidyup(A::QuantumObject, tol::Real=settings.tidyup_tol)
Given a [`QuantumObject`](@ref) `A`, check the real and imaginary parts of each element separately. Remove the real or imaginary value if its absolute value is less than `tol`.
"""
-tidyup(A::QuantumObject{<:AbstractArray{T}}, tol::T2 = 1e-14) where {T,T2<:Real} =
- QuantumObject(tidyup(A.data, tol), A.type, A.dims)
-tidyup(A::AbstractArray{T}, tol::T2 = 1e-14) where {T,T2<:Real} = tidyup!(copy(A), tol)
+tidyup(A::QuantumObject, tol::T = settings.tidyup_tol) where {T<:Real} =
+ QuantumObject(tidyup(A.data, tol), A.type, A.dimensions)
+tidyup(A::AbstractArray, tol::T2 = settings.tidyup_tol) where {T2<:Real} = tidyup!(copy(A), tol)
@doc raw"""
- tidyup!(A::QuantumObject, tol::Real=1e-14)
+ tidyup!(A::QuantumObject, tol::Real=settings.tidyup_tol)
Given a [`QuantumObject`](@ref) `A`, check the real and imaginary parts of each element separately. Remove the real or imaginary value if its absolute value is less than `tol`.
Note that this function is an in-place version of [`tidyup`](@ref).
"""
-tidyup!(A::QuantumObject{<:AbstractArray{T}}, tol::T2 = 1e-14) where {T,T2<:Real} = (tidyup!(A.data, tol); A)
-function tidyup!(A::AbstractSparseArray{T}, tol::T2 = 1e-14) where {T,T2<:Real}
+tidyup!(A::QuantumObject, tol::T = settings.tidyup_tol) where {T<:Real} = (tidyup!(A.data, tol); A)
+function tidyup!(A::AbstractSparseArray, tol::T2 = settings.tidyup_tol) where {T2<:Real}
tidyup!(nonzeros(A), tol) # tidyup A.nzval in-place (also support for CUDA sparse arrays)
return dropzeros!(A)
end
-tidyup!(A::AbstractArray{T}, tol::T2 = 1e-14) where {T<:Real,T2<:Real} = @. A = T(abs(A) > tol) * A
-tidyup!(A::AbstractArray{T}, tol::T2 = 1e-14) where {T,T2<:Real} =
+tidyup!(A::AbstractArray{T}, tol::T2 = settings.tidyup_tol) where {T<:Real,T2<:Real} = @. A = T(abs(A) > tol) * A
+tidyup!(A::AbstractArray{T}, tol::T2 = settings.tidyup_tol) where {T,T2<:Real} =
@. A = T(abs(real(A)) > tol) * real(A) + 1im * T(abs(imag(A)) > tol) * imag(A)
@doc raw"""
- get_data(A::QuantumObject)
+ get_data(A::AbstractQuantumObject)
-Returns the data of a QuantumObject.
+Returns the data of a [`AbstractQuantumObject`](@ref).
"""
-get_data(A::QuantumObject) = A.data
+get_data(A::AbstractQuantumObject) = getfield(A, :data)
@doc raw"""
get_coherence(ψ::QuantumObject)
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))
+function get_coherence(ψ::QuantumObject{Ket})
+ a = destroy(prod(ψ.dimensions))
α = expect(a, ψ)
D = exp(α * a' - conj(α) * a)
return α, D' * ψ
end
-function get_coherence(ρ::QuantumObject{<:AbstractArray,OperatorQuantumObject})
- a = destroy(prod(ρ.dims))
+function get_coherence(ρ::QuantumObject{Operator})
+ a = destroy(prod(ρ.dimensions))
α = expect(a, ρ)
D = exp(α * a' - conj(α) * a)
@@ -665,25 +696,31 @@ 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)
-julia> ψ2 = fock(3, 1)
-julia> ψ3 = fock(4, 2)
-julia> ψ_123 = tensor(ψ1, ψ2, ψ3)
-julia> permute(ψ_123, [2, 1, 3]) ≈ tensor(ψ2, ψ1, ψ3)
+```jldoctest
+julia> ψ1 = fock(2, 0);
+
+julia> ψ2 = fock(3, 1);
+
+julia> ψ3 = fock(4, 2);
+
+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.
+ It is highly recommended to use `permute(A, order)` with `order` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) 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},
+function SparseArrays.permute(
+ A::QuantumObject{ObjType},
order::Union{AbstractVector{Int},Tuple},
-) where {T,ObjType<:Union{KetQuantumObject,BraQuantumObject,OperatorQuantumObject}}
- (length(order) != length(A.dims)) &&
+) where {ObjType<:Union{Ket,Bra,Operator}}
+ any(s -> s isa EnrSpace, A.dimensions.to) && throw(ArgumentError("permute does not support EnrSpace"))
+
+ (length(order) != length(A.dimensions)) &&
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)"))
@@ -695,22 +732,22 @@ function permute(
# obtain the arguments: dims for reshape; perm for PermutedDimsArray
dims, perm = _dims_and_perm(A.type, A.dims, order_svector, length(order_svector))
- return QuantumObject(
- reshape(permutedims(reshape(A.data, dims...), Tuple(perm)), size(A)),
- A.type,
- A.dims[order_svector],
- )
-end
+ order_dimensions = _order_dimensions(A.dimensions, order_svector)
-function _dims_and_perm(
- ::ObjType,
- dims::SVector{N,Int},
- order::AbstractVector{Int},
- L::Int,
-) where {ObjType<:Union{KetQuantumObject,BraQuantumObject},N}
- return reverse(dims), reverse((L + 1) .- order)
+ return QuantumObject(reshape(permutedims(reshape(A.data, dims...), Tuple(perm)), size(A)), A.type, order_dimensions)
end
-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
+_dims_and_perm(::ObjType, dims::SVector{N,Int}, order::AbstractVector{Int}, L::Int) where {ObjType<:Union{Ket,Bra},N} =
+ reverse(dims), reverse((L + 1) .- order)
+
+# if dims originates from Dimensions
+_dims_and_perm(::Operator, dims::SVector{N,Int}, order::AbstractVector{Int}, L::Int) where {N} =
+ reverse(vcat(dims, dims)), reverse((2 * L + 1) .- vcat(order, order .+ L))
+
+# if dims originates from GeneralDimensions
+_dims_and_perm(::Operator, dims::SVector{2,SVector{N,Int}}, order::AbstractVector{Int}, L::Int) where {N} =
+ reverse(vcat(dims[2], dims[1])), reverse((2 * L + 1) .- vcat(order, order .+ L))
+
+_order_dimensions(dimensions::Dimensions, order::AbstractVector{Int}) = Dimensions(dimensions.to[order])
+_order_dimensions(dimensions::GeneralDimensions, order::AbstractVector{Int}) =
+ GeneralDimensions(dimensions.to[order], dimensions.from[order])
diff --git a/src/qobj/block_diagonal_form.jl b/src/qobj/block_diagonal_form.jl
new file mode 100644
index 000000000..13367477b
--- /dev/null
+++ b/src/qobj/block_diagonal_form.jl
@@ -0,0 +1,58 @@
+export BlockDiagonalForm, block_diagonal_form
+
+@doc raw"""
+ struct BlockDiagonalForm
+
+A type for storing a block-diagonal form of a matrix.
+
+# Fields
+- `B::DT`: The block-diagonal matrix. It can be a sparse matrix or a [`QuantumObject`](@ref).
+- `P::DT`: The permutation matrix. It can be a sparse matrix or a [`QuantumObject`](@ref).
+- `blocks::AbstractVector`: The blocks of the block-diagonal matrix.
+- `block_sizes::AbstractVector`: The sizes of the blocks.
+"""
+struct BlockDiagonalForm{DT,BT<:AbstractVector,BST<:AbstractVector}
+ B::DT
+ P::DT
+ blocks::BT
+ block_sizes::BST
+end
+
+function block_diagonal_form(A::MT) where {MT<:AbstractSparseMatrix}
+ n = LinearAlgebra.checksquare(A)
+
+ G = DiGraph(abs.(A))
+ idxs_list = connected_components(G)
+ block_sizes = length.(idxs_list)
+
+ P = MT(sparse(1:n, reduce(vcat, idxs_list), ones(n), n, n))
+
+ blocks = map(eachindex(idxs_list)) do i
+ m = block_sizes[i]
+ idxs = idxs_list[i]
+ P_i = MT(sparse(1:m, idxs, ones(m), m, n))
+ return P_i * A * P_i'
+ end
+
+ B = P * A * P'
+
+ return BlockDiagonalForm(B, P, blocks, block_sizes)
+end
+
+@doc raw"""
+ block_diagonal_form(A::QuantumObject)
+
+Return the block-diagonal form of a [`QuantumObject`](@ref). This is very useful in the presence of symmetries.
+
+# Arguments
+- `A::QuantumObject`: The quantum object.
+
+# Returns
+The [`BlockDiagonalForm`](@ref) of `A`.
+"""
+function block_diagonal_form(A::QuantumObject{OpType}) where {OpType<:Union{Operator,SuperOperator}}
+ bdf = block_diagonal_form(A.data)
+ B = QuantumObject(bdf.B, type = A.type, dims = A.dimensions)
+ P = QuantumObject(bdf.P, type = A.type, dims = A.dimensions)
+ return BlockDiagonalForm(B, P, bdf.blocks, bdf.block_sizes)
+end
diff --git a/src/qobj/boolean_functions.jl b/src/qobj/boolean_functions.jl
index 4ec347679..f460b0965 100644
--- a/src/qobj/boolean_functions.jl
+++ b/src/qobj/boolean_functions.jl
@@ -6,71 +6,77 @@ export isket, isbra, isoper, isoperbra, isoperket, issuper
export isunitary
@doc raw"""
- isbra(A::QuantumObject)
+ isbra(A)
-Checks if the [`QuantumObject`](@ref) `A` is a [`BraQuantumObject`](@ref).
+Checks if the [`QuantumObject`](@ref) `A` is a [`Bra`](@ref). Default case returns `false` for any other inputs.
"""
-isbra(A::QuantumObject{<:AbstractArray{T},OpType}) where {T,OpType<:QuantumObjectType} = OpType <: BraQuantumObject
+isbra(A::QuantumObject{Bra}) = true
+isbra(A) = false # default case
@doc raw"""
- isket(A::QuantumObject)
+ isket(A)
-Checks if the [`QuantumObject`](@ref) `A` is a [`KetQuantumObject`](@ref).
+Checks if the [`QuantumObject`](@ref) `A` is a [`Ket`](@ref). Default case returns `false` for any other inputs.
"""
-isket(A::QuantumObject{<:AbstractArray{T},OpType}) where {T,OpType<:QuantumObjectType} = OpType <: KetQuantumObject
+isket(A::QuantumObject{Ket}) = true
+isket(A) = false # default case
@doc raw"""
- isoper(A::QuantumObject)
+ isoper(A)
-Checks if the [`QuantumObject`](@ref) `A` is a [`OperatorQuantumObject`](@ref).
+Checks if the [`AbstractQuantumObject`](@ref) `A` is a [`Operator`](@ref). Default case returns `false` for any other inputs.
"""
-isoper(A::QuantumObject{<:AbstractArray{T},OpType}) where {T,OpType<:QuantumObjectType} =
- OpType <: OperatorQuantumObject
+isoper(A::AbstractQuantumObject{Operator}) = true
+isoper(A) = false # default case
@doc raw"""
- isoperbra(A::QuantumObject)
+ isoperbra(A)
-Checks if the [`QuantumObject`](@ref) `A` is a [`OperatorBraQuantumObject`](@ref).
+Checks if the [`QuantumObject`](@ref) `A` is a [`OperatorBra`](@ref). Default case returns `false` for any other inputs.
"""
-isoperbra(A::QuantumObject{<:AbstractArray{T},OpType}) where {T,OpType<:QuantumObjectType} =
- OpType <: OperatorBraQuantumObject
+isoperbra(A::QuantumObject{OperatorBra}) = true
+isoperbra(A) = false # default case
@doc raw"""
- isoperket(A::QuantumObject)
+ isoperket(A)
-Checks if the [`QuantumObject`](@ref) `A` is a [`OperatorKetQuantumObject`](@ref).
+Checks if the [`QuantumObject`](@ref) `A` is a [`OperatorKet`](@ref). Default case returns `false` for any other inputs.
"""
-isoperket(A::QuantumObject{<:AbstractArray{T},OpType}) where {T,OpType<:QuantumObjectType} =
- OpType <: OperatorKetQuantumObject
+isoperket(A::QuantumObject{OperatorKet}) = true
+isoperket(A) = false # default case
@doc raw"""
- issuper(A::QuantumObject)
+ issuper(A)
-Checks if the [`QuantumObject`](@ref) `A` is a [`SuperOperatorQuantumObject`](@ref).
+Checks if the [`AbstractQuantumObject`](@ref) `A` is a [`SuperOperator`](@ref). Default case returns `false` for any other inputs.
"""
-issuper(A::QuantumObject{<:AbstractArray{T},OpType}) where {T,OpType<:QuantumObjectType} =
- OpType <: SuperOperatorQuantumObject
+issuper(A::AbstractQuantumObject{<:SuperOperatorType}) = true
+issuper(A) = false # default case
@doc raw"""
- ishermitian(A::QuantumObject)
+ ishermitian(A::AbstractQuantumObject)
+ isherm(A::AbstractQuantumObject)
-Test whether the [`QuantumObject`](@ref) is Hermitian.
+Test whether the [`AbstractQuantumObject`](@ref) is Hermitian.
+
+!!! note
+ `isherm` is a synonym of `ishermitian`.
"""
-LinearAlgebra.ishermitian(A::QuantumObject{<:AbstractArray{T}}) where {T} = ishermitian(A.data)
+LinearAlgebra.ishermitian(A::AbstractQuantumObject) = ishermitian(A.data)
@doc raw"""
- issymmetric(A::QuantumObject)
+ issymmetric(A::AbstractQuantumObject)
-Test whether the [`QuantumObject`](@ref) is symmetric.
+Test whether the [`AbstractQuantumObject`](@ref) is symmetric.
"""
-LinearAlgebra.issymmetric(A::QuantumObject{<:AbstractArray{T}}) where {T} = issymmetric(A.data)
+LinearAlgebra.issymmetric(A::AbstractQuantumObject) = issymmetric(A.data)
@doc raw"""
- isposdef(A::QuantumObject)
+ isposdef(A::AbstractQuantumObject)
-Test whether the [`QuantumObject`](@ref) is positive definite (and Hermitian) by trying to perform a Cholesky factorization of `A`.
+Test whether the [`AbstractQuantumObject`](@ref) is positive definite (and Hermitian) by trying to perform a Cholesky factorization of `A`.
"""
-LinearAlgebra.isposdef(A::QuantumObject{<:AbstractArray{T}}) where {T} = isposdef(A.data)
+LinearAlgebra.isposdef(A::AbstractQuantumObject) = isposdef(A.data)
@doc raw"""
isunitary(U::QuantumObject; kwargs...)
@@ -79,5 +85,18 @@ Test whether the [`QuantumObject`](@ref) ``U`` is unitary operator. This functio
Note that all the keyword arguments will be passed to `Base.isapprox`.
"""
-isunitary(U::QuantumObject{<:AbstractArray{T}}; kwargs...) where {T} =
- isoper(U) ? isapprox(U.data * U.data', I(size(U, 1)); kwargs...) : false
+isunitary(U::QuantumObject; kwargs...) = isoper(U) ? isapprox(U.data * U.data', I(size(U, 1)); kwargs...) : false
+
+@doc raw"""
+ SciMLOperators.iscached(A::AbstractQuantumObject)
+
+Test whether the [`AbstractQuantumObject`](@ref) `A` has preallocated caches for inplace evaluations.
+"""
+SciMLOperators.iscached(A::AbstractQuantumObject) = iscached(A.data)
+
+@doc raw"""
+ SciMLOperators.isconstant(A::AbstractQuantumObject)
+
+Test whether the [`AbstractQuantumObject`](@ref) `A` is constant in time. For a [`QuantumObject`](@ref), this function returns `true`, while for a [`QuantumObjectEvolution`](@ref), this function returns `true` if the operator is constant in time.
+"""
+SciMLOperators.isconstant(A::AbstractQuantumObject) = isconstant(A.data)
diff --git a/src/qobj/dimensions.jl b/src/qobj/dimensions.jl
new file mode 100644
index 000000000..692da0f05
--- /dev/null
+++ b/src/qobj/dimensions.jl
@@ -0,0 +1,104 @@
+#=
+This file defines the Dimensions structures, which can describe composite Hilbert spaces.
+=#
+
+export AbstractDimensions, Dimensions, GeneralDimensions
+
+abstract type AbstractDimensions{M,N} end
+
+@doc raw"""
+ struct Dimensions{N,T<:Tuple} <: AbstractDimensions{N, N}
+ to::T
+ end
+
+A structure that describes the Hilbert [`Space`](@ref) of each subsystems.
+"""
+struct Dimensions{N,T<:Tuple} <: AbstractDimensions{N,N}
+ to::T
+
+ # make sure the elements in the tuple are all AbstractSpace
+ Dimensions(to::NTuple{N,AbstractSpace}) where {N} = new{N,typeof(to)}(to)
+end
+function Dimensions(dims::Union{AbstractVector{T},NTuple{N,T}}) where {T<:Integer,N}
+ _non_static_array_warning("dims", dims)
+ L = length(dims)
+ (L > 0) || throw(DomainError(dims, "The argument dims must be of non-zero length"))
+
+ return Dimensions(Tuple(Space.(dims)))
+end
+Dimensions(dims::Int) = Dimensions(Space(dims))
+Dimensions(dims::DimType) where {DimType<:AbstractSpace} = Dimensions((dims,))
+Dimensions(dims::Any) = throw(
+ ArgumentError(
+ "The argument dims must be a Tuple or a StaticVector of non-zero length and contain only positive integers.",
+ ),
+)
+
+@doc raw"""
+ struct GeneralDimensions{N,T1<:Tuple,T2<:Tuple} <: AbstractDimensions{N}
+ to::T1
+ from::T2
+ end
+
+A structure that describes the left-hand side (`to`) and right-hand side (`from`) Hilbert [`Space`](@ref) of an [`Operator`](@ref).
+"""
+struct GeneralDimensions{M,N,T1<:Tuple,T2<:Tuple} <: AbstractDimensions{M,N}
+ to::T1 # space acting on the left
+ from::T2 # space acting on the right
+
+ # make sure the elements in the tuple are all AbstractSpace
+ GeneralDimensions(to::NTuple{M,AbstractSpace}, from::NTuple{N,AbstractSpace}) where {M,N} =
+ new{M,N,typeof(to),typeof(from)}(to, from)
+end
+function GeneralDimensions(dims::Union{AbstractVector{T},NTuple{N,T}}) where {T<:Union{AbstractVector,NTuple},N}
+ (length(dims) != 2) && throw(ArgumentError("Invalid dims = $dims"))
+
+ _non_static_array_warning("dims[1]", dims[1])
+ _non_static_array_warning("dims[2]", dims[2])
+
+ L1 = length(dims[1])
+ L2 = length(dims[2])
+ (L1 > 0) || throw(DomainError(L1, "The length of `dims[1]` must be larger or equal to 1."))
+ (L2 > 0) || throw(DomainError(L2, "The length of `dims[2]` must be larger or equal to 1."))
+
+ return GeneralDimensions(Tuple(Space.(dims[1])), Tuple(Space.(dims[2])))
+end
+
+_gen_dimensions(dims::AbstractDimensions) = dims
+_gen_dimensions(dims::Union{AbstractVector{T},NTuple{N,T}}) where {T<:Integer,N} = Dimensions(dims)
+_gen_dimensions(dims::Union{AbstractVector{T},NTuple{N,T}}) where {T<:Union{AbstractVector,NTuple},N} =
+ GeneralDimensions(dims)
+_gen_dimensions(dims::Any) = Dimensions(dims)
+
+# obtain dims in the type of SVector with integers
+dimensions_to_dims(dimensions::NTuple{N,AbstractSpace}) where {N} = vcat(map(dimensions_to_dims, dimensions)...)
+dimensions_to_dims(dimensions::Dimensions) = dimensions_to_dims(dimensions.to)
+dimensions_to_dims(dimensions::GeneralDimensions) =
+ SVector{2}(dimensions_to_dims(dimensions.to), dimensions_to_dims(dimensions.from))
+
+dimensions_to_dims(::Nothing) = nothing # for EigsolveResult.dimensions = nothing
+
+Base.length(::AbstractDimensions{N}) where {N} = N
+
+# need to specify return type `Int` for `_get_space_size`
+# otherwise the type of `prod(::Dimensions)` will be unstable
+_get_space_size(s::AbstractSpace)::Int = s.size
+Base.prod(dims::Dimensions) = prod(dims.to)
+Base.prod(spaces::NTuple{N,AbstractSpace}) where {N} = prod(_get_space_size, spaces)
+
+Base.transpose(dimensions::Dimensions) = dimensions
+Base.transpose(dimensions::GeneralDimensions) = GeneralDimensions(dimensions.from, dimensions.to) # switch `to` and `from`
+Base.adjoint(dimensions::AbstractDimensions) = transpose(dimensions)
+
+# this is used to show `dims` for Qobj and QobjEvo
+_get_dims_string(dimensions::Dimensions) = string(dimensions_to_dims(dimensions))
+function _get_dims_string(dimensions::GeneralDimensions)
+ dims = dimensions_to_dims(dimensions)
+ return "[$(string(dims[1])), $(string(dims[2]))]"
+end
+_get_dims_string(::Nothing) = "nothing" # for EigsolveResult.dimensions = nothing
+
+Base.:(==)(dim1::Dimensions, dim2::Dimensions) = dim1.to == dim2.to
+Base.:(==)(dim1::GeneralDimensions, dim2::GeneralDimensions) = (dim1.to == dim2.to) && (dim1.from == dim2.from)
+Base.:(==)(dim1::Dimensions, dim2::GeneralDimensions) = false
+Base.:(==)(dim1::GeneralDimensions, dim2::Dimensions) = false
diff --git a/src/qobj/eigsolve.jl b/src/qobj/eigsolve.jl
index dee88be26..bac1a8899 100644
--- a/src/qobj/eigsolve.jl
+++ b/src/qobj/eigsolve.jl
@@ -3,36 +3,41 @@ Eigen solvers and results for QuantumObject
=#
export EigsolveResult
-export eigenenergies, eigenstates, eigsolve, eigsolve_al
+export eigenenergies, eigenstates, eigsolve
+export eigsolve_al
@doc raw"""
- struct EigsolveResult{T1<:Vector{<:Number}, T2<:AbstractMatrix{<:Number}, ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject},N}
- values::T1
- vectors::T2
- type::ObjType
- dims::SVector{N,Int}
- iter::Int
- numops::Int
- converged::Bool
- end
+ struct EigsolveResult
A struct containing the eigenvalues, the eigenvectors, and some information from the solver
-# Fields
+# Fields (Attributes)
- `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::SVector`: the `dims` of [`QuantumObject`](@ref)
+- `dimensions::Union{Nothing,AbstractDimensions}`: the `dimensions` of [`QuantumObject`](@ref), or `nothing` means solving eigen equation for general matrix
- `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
+!!! note "`dims` property"
+ For a given `res::EigsolveResult`, `res.dims` or `getproperty(res, :dims)` returns its `dimensions` in the type of integer-vector.
+
# Examples
One can obtain the eigenvalues and the corresponding [`QuantumObject`](@ref)-type eigenvectors by:
-```
-julia> result = eigenstates(sigmax());
+```jldoctest
+julia> result = eigenstates(sigmax())
+EigsolveResult: type=Operator() dims=[2]
+values:
+2-element Vector{ComplexF64}:
+ -1.0 + 0.0im
+ 1.0 + 0.0im
+vectors:
+2×2 Matrix{ComplexF64}:
+ -0.707107+0.0im 0.707107+0.0im
+ 0.707107+0.0im 0.707107+0.0im
-julia> λ, ψ, T = result;
+julia> λ, ψ, U = result;
julia> λ
2-element Vector{ComplexF64}:
@@ -40,11 +45,19 @@ julia> λ
1.0 + 0.0im
julia> ψ
-2-element Vector{QuantumObject{Vector{ComplexF64}, KetQuantumObject}}:
- QuantumObject{Vector{ComplexF64}, KetQuantumObject}(ComplexF64[-0.7071067811865475 + 0.0im, 0.7071067811865475 + 0.0im], KetQuantumObject(), [2])
- QuantumObject{Vector{ComplexF64}, KetQuantumObject}(ComplexF64[0.7071067811865475 + 0.0im, 0.7071067811865475 + 0.0im], KetQuantumObject(), [2])
+2-element Vector{QuantumObject{Ket, Dimensions{1, Tuple{Space}}, Vector{ComplexF64}}}:
+
+Quantum Object: type=Ket() dims=[2] size=(2,)
+2-element Vector{ComplexF64}:
+ -0.7071067811865475 + 0.0im
+ 0.7071067811865475 + 0.0im
+
+Quantum Object: type=Ket() dims=[2] size=(2,)
+2-element Vector{ComplexF64}:
+ 0.7071067811865475 + 0.0im
+ 0.7071067811865475 + 0.0im
-julia> T
+julia> U
2×2 Matrix{ComplexF64}:
-0.707107+0.0im 0.707107+0.0im
0.707107+0.0im 0.707107+0.0im
@@ -53,30 +66,39 @@ julia> T
struct EigsolveResult{
T1<:Vector{<:Number},
T2<:AbstractMatrix{<:Number},
- ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject},
- N,
+ ObjType<:Union{Nothing,Operator,SuperOperator},
+ DimType<:Union{Nothing,AbstractDimensions},
}
values::T1
vectors::T2
type::ObjType
- dims::SVector{N,Int}
+ dimensions::DimType
iter::Int
numops::Int
converged::Bool
end
+function Base.getproperty(res::EigsolveResult, key::Symbol)
+ # a comment here to avoid bad render by JuliaFormatter
+ if key === :dims
+ return dimensions_to_dims(getfield(res, :dimensions))
+ else
+ return getfield(res, key)
+ end
+end
+
Base.iterate(res::EigsolveResult) = (res.values, Val(:vector_list))
Base.iterate(res::EigsolveResult{T1,T2,Nothing}, ::Val{:vector_list}) where {T1,T2} =
([res.vectors[:, k] for k in 1:length(res.values)], Val(:vectors))
-Base.iterate(res::EigsolveResult{T1,T2,OperatorQuantumObject}, ::Val{:vector_list}) where {T1,T2} =
- ([QuantumObject(res.vectors[:, k], Ket, res.dims) for k in 1:length(res.values)], Val(:vectors))
-Base.iterate(res::EigsolveResult{T1,T2,SuperOperatorQuantumObject}, ::Val{:vector_list}) where {T1,T2} =
- ([QuantumObject(res.vectors[:, k], OperatorKet, res.dims) for k in 1:length(res.values)], Val(:vectors))
+Base.iterate(res::EigsolveResult{T1,T2,Operator}, ::Val{:vector_list}) where {T1,T2} =
+ ([QuantumObject(res.vectors[:, k], Ket(), res.dimensions) for k in 1:length(res.values)], Val(:vectors))
+Base.iterate(res::EigsolveResult{T1,T2,SuperOperator}, ::Val{:vector_list}) where {T1,T2} =
+ ([QuantumObject(res.vectors[:, k], OperatorKet(), res.dimensions) for k in 1:length(res.values)], Val(:vectors))
Base.iterate(res::EigsolveResult, ::Val{:vectors}) = (res.vectors, Val(:done))
Base.iterate(res::EigsolveResult, ::Val{:done}) = nothing
function Base.show(io::IO, res::EigsolveResult)
- println(io, "EigsolveResult: type=", res.type, " dims=", res.dims)
+ println(io, "EigsolveResult: type=", res.type, " dims=", _get_dims_string(res.dimensions))
println(io, "values:")
show(io, MIME("text/plain"), res.values)
print(io, "\n")
@@ -127,12 +149,12 @@ function _permuteschur!(
return T, Q
end
-function _update_schur_eigs!(Hₘ, Uₘ, Uₘᵥ, f, m, β, sorted_vals)
+function _update_schur_eigs!(Hₘ, Uₘ, Uₘᵥ, f, m, β, sorted_vals, sortby, rev)
F = hessenberg!(Hₘ)
copyto!(Uₘ, Hₘ)
LAPACK.orghr!(1, m, Uₘ, F.τ)
Tₘ, Uₘ, values = hseqr!(Hₘ, Uₘ)
- sortperm!(sorted_vals, values, by = abs, rev = true)
+ sortperm!(sorted_vals, values, by = sortby, rev = rev)
_permuteschur!(Tₘ, Uₘ, sorted_vals)
mul!(f, Uₘᵥ, β)
@@ -143,12 +165,14 @@ function _eigsolve(
A,
b::AbstractVector{T},
type::ObjType,
- dims::SVector,
+ dimensions::Union{Nothing,AbstractDimensions},
k::Int = 1,
m::Int = max(20, 2 * k + 1);
tol::Real = 1e-8,
maxiter::Int = 200,
-) where {T<:BlasFloat,ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject}}
+ sortby::Function = abs2,
+ rev = true,
+) where {T<:BlasFloat,ObjType<:Union{Nothing,Operator,SuperOperator}}
n = size(A, 2)
V = similar(b, n, m + 1)
H = zeros(T, m + 1, m)
@@ -187,7 +211,7 @@ function _eigsolve(
M = typeof(cache0)
- Tₘ, Uₘ = _update_schur_eigs!(Hₘ, Uₘ, Uₘᵥ, f, m, β, sorted_vals)
+ Tₘ, Uₘ = _update_schur_eigs!(Hₘ, Uₘ, Uₘᵥ, f, m, β, sorted_vals, sortby, rev)
numops = m
iter = 0
@@ -203,7 +227,7 @@ function _eigsolve(
# println( A * view(V, :, 1:k) ≈ view(V, :, 1:k) * M(view(H, 1:k, 1:k)) + qₘ * M(transpose(view(transpose(βeₘ) * Uₘ, 1:k))) ) # SHOULD BE TRUE
- for j in k+1:m
+ for j in (k+1):m
β = arnoldi_step!(A, V, H, j)
if β < tol
numops += j - k - 1
@@ -213,7 +237,7 @@ function _eigsolve(
# println( A * Vₘ ≈ Vₘ * M(Hₘ) + qₘ * M(transpose(βeₘ)) ) # SHOULD BE TRUE
- Tₘ, Uₘ = _update_schur_eigs!(Hₘ, Uₘ, Uₘᵥ, f, m, β, sorted_vals)
+ Tₘ, Uₘ = _update_schur_eigs!(Hₘ, Uₘ, Uₘᵥ, f, m, β, sorted_vals, sortby, rev)
numops += m - k - 1
iter += 1
@@ -227,23 +251,39 @@ function _eigsolve(
end
mul!(cache1, Vₘ, M(Uₘ * VR))
vecs = cache1[:, 1:k]
+ settings.auto_tidyup && tidyup!(vecs)
- return EigsolveResult(vals, vecs, type, dims, iter, numops, (iter < maxiter))
+ return EigsolveResult(vals, vecs, type, dimensions, iter, numops, (iter < maxiter))
end
@doc raw"""
eigsolve(A::QuantumObject;
v0::Union{Nothing,AbstractVector}=nothing,
sigma::Union{Nothing, Real}=nothing,
- k::Int = 1,
+ eigvals::Int = 1,
krylovdim::Int = max(20, 2*k+1),
tol::Real = 1e-8,
maxiter::Int = 200,
solver::Union{Nothing, SciMLLinearSolveAlgorithm} = nothing,
+ sortby::Function = abs2,
+ rev::Bool = true,
kwargs...)
Solve for the eigenvalues and eigenvectors of a matrix `A` using the Arnoldi method.
+# Arguments
+- `A::QuantumObject`: the [`QuantumObject`](@ref) to solve eigenvalues and eigenvectors.
+- `v0::Union{Nothing,AbstractVector}`: the initial vector for the Arnoldi method. Default is a random vector.
+- `sigma::Union{Nothing, Real}`: the shift for the eigenvalue problem. Default is `nothing`.
+- `eigvals::Int`: the number of eigenvalues to compute. Default is `1`.
+- `krylovdim::Int`: the dimension of the Krylov subspace. Default is `max(20, 2*k+1)`.
+- `tol::Real`: the tolerance for the Arnoldi method. Default is `1e-8`.
+- `maxiter::Int`: the maximum number of iterations for the Arnoldi method. Default is `200`.
+- `solver::Union{Nothing, SciMLLinearSolveAlgorithm}`: the linear solver algorithm. Default is `nothing`.
+- `sortby::Function`: the function to sort eigenvalues. Default is `abs2`.
+- `rev::Bool`: whether to sort in descending order. Default is `true`.
+- `kwargs`: Additional keyword arguments passed to the solver.
+
# Notes
- For more details about `solver` and extra `kwargs`, please refer to [`LinearSolve.jl`](https://docs.sciml.ai/LinearSolve/stable/)
@@ -251,27 +291,31 @@ Solve for the eigenvalues and eigenvectors of a matrix `A` using the Arnoldi met
- `EigsolveResult`: A struct containing the eigenvalues, the eigenvectors, and some information about the eigsolver
"""
function eigsolve(
- A::QuantumObject{<:AbstractMatrix};
+ A::QuantumObject;
v0::Union{Nothing,AbstractVector} = nothing,
sigma::Union{Nothing,Real} = nothing,
- k::Int = 1,
- krylovdim::Int = max(20, 2 * k + 1),
+ eigvals::Int = 1,
+ krylovdim::Int = max(20, 2 * eigvals + 1),
tol::Real = 1e-8,
maxiter::Int = 200,
solver::Union{Nothing,SciMLLinearSolveAlgorithm} = nothing,
+ sortby::Function = abs2,
+ rev::Bool = true,
kwargs...,
)
return eigsolve(
A.data;
v0 = v0,
type = A.type,
- dims = A.dims,
+ dimensions = A.dimensions,
sigma = sigma,
- k = k,
+ eigvals = eigvals,
krylovdim = krylovdim,
tol = tol,
maxiter = maxiter,
solver = solver,
+ sortby = sortby,
+ rev = rev,
kwargs...,
)
end
@@ -279,24 +323,35 @@ end
function eigsolve(
A;
v0::Union{Nothing,AbstractVector} = nothing,
- type::Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject} = nothing,
- dims = SVector{0,Int}(),
+ type::Union{Nothing,Operator,SuperOperator} = nothing,
+ dimensions = nothing,
sigma::Union{Nothing,Real} = nothing,
- k::Int = 1,
- krylovdim::Int = max(20, 2 * k + 1),
+ eigvals::Int = 1,
+ krylovdim::Int = max(20, 2 * eigvals + 1),
tol::Real = 1e-8,
maxiter::Int = 200,
solver::Union{Nothing,SciMLLinearSolveAlgorithm} = nothing,
+ sortby::Function = abs2,
+ rev::Bool = true,
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)
+ res = _eigsolve(
+ A,
+ v0,
+ type,
+ dimensions,
+ eigvals,
+ krylovdim,
+ tol = tol,
+ maxiter = maxiter,
+ sortby = sortby,
+ rev = rev,
+ )
vals = res.values
else
Aₛ = A - sigma * I
@@ -314,41 +369,60 @@ function eigsolve(
Amap = EigsolveInverseMap(T, size(A), linsolve)
- res = _eigsolve(Amap, v0, type, dims, k, krylovdim, tol = tol, maxiter = maxiter)
+ res = _eigsolve(
+ Amap,
+ v0,
+ type,
+ dimensions,
+ eigvals,
+ krylovdim,
+ tol = tol,
+ maxiter = maxiter,
+ sortby = sortby,
+ rev = rev,
+ )
vals = @. (1 + sigma * res.values) / res.values
end
- return EigsolveResult(vals, res.vectors, res.type, res.dims, res.iter, res.numops, res.converged)
+ vecs = res.vectors
+ settings.auto_tidyup && tidyup!(vecs)
+
+ return EigsolveResult(vals, vecs, res.type, res.dimensions, res.iter, res.numops, res.converged)
end
@doc raw"""
- eigsolve_al(H::QuantumObject,
- T::Real, c_ops::Union{Nothing,AbstractVector}=nothing;
- alg::OrdinaryDiffEqAlgorithm=Tsit5(),
- H_t::Union{Nothing,Function}=nothing,
- params::NamedTuple=NamedTuple(),
- ρ0::Union{Nothing, AbstractMatrix} = nothing,
- k::Int=1,
- krylovdim::Int=min(10, size(H, 1)),
- maxiter::Int=200,
- eigstol::Real=1e-6,
- kwargs...)
+ eigsolve_al(
+ H::Union{AbstractQuantumObject{HOpType},Tuple},
+ T::Real,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ alg::OrdinaryDiffEqAlgorithm = Tsit5(),
+ params::NamedTuple = NamedTuple(),
+ ρ0::AbstractMatrix = rand_dm(prod(H.dimensions)).data,
+ eigvals::Int = 1,
+ krylovdim::Int = min(10, size(H, 1)),
+ maxiter::Int = 200,
+ eigstol::Real = 1e-6,
+ sortby::Function = abs2,
+ rev::Bool = true,
+ kwargs...,
+ )
Solve the eigenvalue problem for a Liouvillian superoperator `L` using the Arnoldi-Lindblad method.
# Arguments
-- `H`: The Hamiltonian (or directly the Liouvillian) of the system.
-- `T`: The time at which to evaluate the time evolution
+- `H`: The Hamiltonian (or directly the Liouvillian) of the system. It can be a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a tuple of the form supported by [`mesolve`](@ref).
+- `T`: The time at which to evaluate the time evolution.
- `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
-- `ρ0`: The initial density matrix. If not specified, a random density matrix is used
-- `k`: The number of eigenvalues to compute
-- `krylovdim`: The dimension of the Krylov subspace
-- `maxiter`: The maximum number of iterations for the eigsolver
-- `eigstol`: The tolerance for the eigsolver
-- `kwargs`: Additional keyword arguments passed to the differential equation solver
+- `alg`: The differential equation solver algorithm. Default is `Tsit5()`.
+- `params`: A `NamedTuple` containing the parameters of the system.
+- `ρ0`: The initial density matrix. If not specified, a random density matrix is used.
+- `eigvals`: The number of eigenvalues to compute.
+- `krylovdim`: The dimension of the Krylov subspace.
+- `maxiter`: The maximum number of iterations for the eigsolver.
+- `eigstol`: The tolerance for the eigsolver.
+- `sortby::Function`: the function to sort eigenvalues. Default is `abs2`.
+- `rev::Bool`: whether to sort in descending order. Default is `true`.
+- `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/)
@@ -361,49 +435,58 @@ Solve the eigenvalue problem for a Liouvillian superoperator `L` using the Arnol
- [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},
+ H::Union{AbstractQuantumObject{HOpType},Tuple},
T::Real,
- c_ops::Union{Nothing,AbstractVector} = nothing;
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- H_t::Union{Nothing,Function} = nothing,
params::NamedTuple = NamedTuple(),
- ρ0::AbstractMatrix = rand_dm(prod(H.dims)).data,
- k::Int = 1,
+ ρ0::AbstractMatrix = rand_dm(prod(H.dimensions)).data,
+ eigvals::Int = 1,
krylovdim::Int = min(10, size(H, 1)),
maxiter::Int = 200,
eigstol::Real = 1e-6,
+ sortby::Function = abs2,
+ rev::Bool = true,
kwargs...,
-) where {MT1<:AbstractMatrix,HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
- L = liouvillian(H, c_ops)
+) where {HOpType<:Union{Operator,SuperOperator}}
+ L_evo = _mesolve_make_L_QobjEvo(H, c_ops)
prob = mesolveProblem(
- L,
- QuantumObject(ρ0, dims = H.dims),
- [0, T];
- alg = alg,
- H_t = H_t,
+ L_evo,
+ QuantumObject(ρ0, type = Operator(), dims = H.dimensions),
+ [zero(T), T];
params = params,
progress_bar = Val(false),
kwargs...,
- )
+ ).prob
integrator = init(prob, alg)
- # prog = ProgressUnknown(desc="Applications:", showspeed = true, enabled=progress)
-
- Lmap = ArnoldiLindbladIntegratorMap(eltype(MT1), size(L), integrator)
+ Lmap = ArnoldiLindbladIntegratorMap(eltype(H), size(L_evo), integrator)
- res = _eigsolve(Lmap, mat2vec(ρ0), L.type, L.dims, k, krylovdim, maxiter = maxiter, tol = eigstol)
- # finish!(prog)
+ res = _eigsolve(
+ Lmap,
+ mat2vec(ρ0),
+ L_evo.type,
+ L_evo.dimensions,
+ eigvals,
+ krylovdim,
+ maxiter = maxiter,
+ tol = eigstol,
+ sortby = sortby,
+ rev = rev,
+ )
vals = similar(res.values)
vecs = similar(res.vectors)
for i in eachindex(res.values)
vec = view(res.vectors, :, i)
- vals[i] = dot(vec, L.data, vec)
+ vals[i] = dot(vec, L_evo.data, vec)
@. vecs[:, i] = vec * exp(-1im * angle(vec[1]))
end
- return EigsolveResult(vals, vecs, res.type, res.dims, res.iter, res.numops, res.converged)
+ settings.auto_tidyup && tidyup!(vecs)
+
+ return EigsolveResult(vals, vecs, res.type, res.dimensions, res.iter, res.numops, res.converged)
end
@doc raw"""
@@ -412,49 +495,43 @@ end
Calculates the eigenvalues and eigenvectors of the [`QuantumObject`](@ref) `A` using
the Julia [LinearAlgebra](https://docs.julialang.org/en/v1/stdlib/LinearAlgebra/) package.
-```
+```jldoctest
julia> a = destroy(5);
-julia> H = a + a'
-Quantum Object: type=Operator dims=[5] size=(5, 5) ishermitian=true
-5×5 SparseMatrixCSC{ComplexF64, Int64} with 8 stored entries:
- ⋅ 1.0+0.0im ⋅ ⋅ ⋅
- 1.0+0.0im ⋅ 1.41421+0.0im ⋅ ⋅
- ⋅ 1.41421+0.0im ⋅ 1.73205+0.0im ⋅
- ⋅ ⋅ 1.73205+0.0im ⋅ 2.0+0.0im
- ⋅ ⋅ ⋅ 2.0+0.0im ⋅
+julia> H = a + a';
+
+julia> using LinearAlgebra;
julia> E, ψ, U = eigen(H)
-EigsolveResult: type=Operator dims=[5]
+EigsolveResult: type=Operator() dims=[5]
values:
-5-element Vector{Float64}:
- -2.8569700138728
- -1.3556261799742608
- 1.3322676295501878e-15
- 1.3556261799742677
- 2.8569700138728056
+5-element Vector{ComplexF64}:
+ -2.8569700138728 + 0.0im
+ -1.3556261799742608 + 0.0im
+ 1.3322676295501878e-15 + 0.0im
+ 1.3556261799742677 + 0.0im
+ 2.8569700138728056 + 0.0im
vectors:
5×5 Matrix{ComplexF64}:
- 0.106101+0.0im -0.471249-0.0im … 0.471249-0.0im 0.106101-0.0im
- -0.303127-0.0im 0.638838+0.0im 0.638838+0.0im 0.303127-0.0im
- 0.537348+0.0im -0.279149-0.0im 0.279149-0.0im 0.537348-0.0im
+ 0.106101+0.0im -0.471249-0.0im … 0.471249+0.0im 0.106101+0.0im
+ -0.303127-0.0im 0.638838+0.0im 0.638838+0.0im 0.303127+0.0im
+ 0.537348+0.0im -0.279149-0.0im 0.279149+0.0im 0.537348+0.0im
-0.638838-0.0im -0.303127-0.0im -0.303127-0.0im 0.638838+0.0im
- 0.447214+0.0im 0.447214+0.0im -0.447214-0.0im 0.447214-0.0im
+ 0.447214+0.0im 0.447214+0.0im -0.447214-0.0im 0.447214+0.0im
julia> expect(H, ψ[1]) ≈ E[1]
true
```
"""
-function LinearAlgebra.eigen(
- A::QuantumObject{MT,OpType};
- kwargs...,
-) where {MT<:AbstractMatrix,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
- F = eigen(sparse_to_dense(A.data); kwargs...)
+function LinearAlgebra.eigen(A::QuantumObject{OpType}; kwargs...) where {OpType<:Union{Operator,SuperOperator}}
+ MT = typeof(A.data)
+ F = eigen(to_dense(A.data); kwargs...)
# This fixes a type inference issue. But doesn't work for GPU arrays
- E::mat2vec(sparse_to_dense(MT)) = F.values
- U::sparse_to_dense(MT) = F.vectors
+ E::mat2vec(to_dense(MT)) = F.values
+ U::to_dense(MT) = F.vectors
+ settings.auto_tidyup && tidyup!(U)
- return EigsolveResult(E, U, A.type, A.dims, 0, 0, true)
+ return EigsolveResult(E, U, A.type, A.dimensions, 0, 0, true)
end
@doc raw"""
@@ -462,11 +539,8 @@ end
Same as [`eigen(A::QuantumObject; kwargs...)`](@ref) but for only the eigenvalues.
"""
-LinearAlgebra.eigvals(
- A::QuantumObject{<:AbstractArray{T},OpType};
- kwargs...,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- eigvals(sparse_to_dense(A.data); kwargs...)
+LinearAlgebra.eigvals(A::QuantumObject{OpType}; kwargs...) where {OpType<:Union{Operator,SuperOperator}} =
+ eigvals(to_dense(A.data); kwargs...)
@doc raw"""
eigenenergies(A::QuantumObject; sparse::Bool=false, kwargs...)
@@ -476,16 +550,16 @@ Calculate the eigenenergies
# Arguments
- `A::QuantumObject`: the [`QuantumObject`](@ref) to solve eigenvalues
- `sparse::Bool`: if `false` call [`eigvals(A::QuantumObject; kwargs...)`](@ref), otherwise call [`eigsolve`](@ref). Default to `false`.
-- `kwargs`: Additional keyword arguments passed to the solver
+- `kwargs`: Additional keyword arguments passed to the solver. If `sparse=true`, the keyword arguments are passed to [`eigsolve`](@ref), otherwise to [`eigen`](@ref).
# Returns
- `::Vector{<:Number}`: a list of eigenvalues
"""
function eigenenergies(
- A::QuantumObject{<:AbstractArray{T},OpType};
+ A::QuantumObject{OpType};
sparse::Bool = false,
kwargs...,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
+) where {OpType<:Union{Operator,SuperOperator}}
if !sparse
return eigvals(A; kwargs...)
else
@@ -501,16 +575,16 @@ Calculate the eigenvalues and corresponding eigenvectors
# Arguments
- `A::QuantumObject`: the [`QuantumObject`](@ref) to solve eigenvalues and eigenvectors
- `sparse::Bool`: if `false` call [`eigen(A::QuantumObject; kwargs...)`](@ref), otherwise call [`eigsolve`](@ref). Default to `false`.
-- `kwargs`: Additional keyword arguments passed to the solver
+- `kwargs`: Additional keyword arguments passed to the solver. If `sparse=true`, the keyword arguments are passed to [`eigsolve`](@ref), otherwise to [`eigen`](@ref).
# Returns
- `::EigsolveResult`: containing the eigenvalues, the eigenvectors, and some information from the solver. see also [`EigsolveResult`](@ref)
"""
function eigenstates(
- A::QuantumObject{<:AbstractArray{T},OpType};
+ A::QuantumObject{OpType};
sparse::Bool = false,
kwargs...,
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
+) where {OpType<:Union{Operator,SuperOperator}}
if !sparse
return eigen(A; kwargs...)
else
diff --git a/src/qobj/energy_restricted.jl b/src/qobj/energy_restricted.jl
new file mode 100644
index 000000000..f65ea84ca
--- /dev/null
+++ b/src/qobj/energy_restricted.jl
@@ -0,0 +1,264 @@
+#=
+This file defines the energy restricted space structure.
+=#
+
+export EnrSpace, enr_state_dictionaries
+export enr_fock, enr_thermal_dm, enr_destroy, enr_identity
+
+@doc raw"""
+ struct EnrSpace{N} <: AbstractSpace
+ size::Int
+ dims::NTuple{N,Int}
+ n_excitations::Int
+ state2idx::Dict{SVector{N,Int},Int}
+ idx2state::Dict{Int,SVector{N,Int}}
+ end
+
+A structure that describes an excitation number restricted (ENR) state space, where `N` is the number of sub-systems.
+
+# Fields
+
+- `size`: Number of states in the excitation number restricted state space
+- `dims`: A list of the number of states in each sub-system
+- `n_excitations`: Maximum number of excitations
+- `state2idx`: A dictionary for looking up a state index from a state (`SVector`)
+- `idx2state`: A dictionary for looking up state (`SVector`) from a state index
+
+# Functions
+
+With this `EnrSpace`, one can use the following functions to construct states or operators in the excitation number restricted (ENR) space:
+
+- [`enr_fock`](@ref)
+- [`enr_thermal_dm`](@ref)
+- [`enr_destroy`](@ref)
+- [`enr_identity`](@ref)
+
+# Example
+
+To construct an `EnrSpace`, we only need to specify the `dims` and `n_excitations`, namely
+
+```jldoctest
+julia> dims = (2, 2, 3);
+
+julia> n_excitations = 3;
+
+julia> EnrSpace(dims, n_excitations)
+EnrSpace([2, 2, 3], 3)
+```
+
+!!! warning "Beware of type-stability!"
+ It is highly recommended to use `EnrSpace(dims, n_excitations)` with `dims` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) to keep type stability. 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.
+"""
+struct EnrSpace{N} <: AbstractSpace
+ size::Int
+ dims::SVector{N,Int}
+ n_excitations::Int
+ state2idx::Dict{SVector{N,Int},Int}
+ idx2state::Dict{Int,SVector{N,Int}}
+
+ function EnrSpace(dims::Union{AbstractVector{T},NTuple{N,T}}, n_excitations::Int) where {T<:Integer,N}
+ # all arguments will be checked in `enr_state_dictionaries`
+ size, state2idx, idx2state = enr_state_dictionaries(dims, n_excitations)
+
+ L = length(dims)
+ return new{L}(size, SVector{L}(dims), n_excitations, state2idx, idx2state)
+ end
+end
+
+function Base.show(io::IO, s::EnrSpace)
+ print(io, "EnrSpace($(s.dims), $(s.n_excitations))")
+ return nothing
+end
+
+Base.:(==)(s_enr1::EnrSpace, s_enr2::EnrSpace) = (s_enr1.size == s_enr2.size) && (s_enr1.dims == s_enr2.dims)
+
+dimensions_to_dims(s_enr::EnrSpace) = s_enr.dims
+
+@doc raw"""
+ enr_state_dictionaries(dims, n_excitations)
+
+Return the number of states, and lookup-dictionaries for translating a state (`SVector`) to a state index, and vice versa, for a system with a given number of components and maximum number of excitations.
+
+# Arguments
+- `dims::Union{AbstractVector,Tuple}`: A list of the number of states in each sub-system
+- `n_excitations::Int`: Maximum number of excitations
+
+# Returns
+- `nstates`: Number of states
+- `state2idx`: A dictionary for looking up a state index from a state (`SVector`)
+- `idx2state`: A dictionary for looking up state (`SVector`) from a state index
+"""
+function enr_state_dictionaries(dims::Union{AbstractVector{T},NTuple{N,T}}, n_excitations::Int) where {T<:Integer,N}
+ # argument checks
+ _non_static_array_warning("dims", dims)
+ L = length(dims)
+ (L > 0) || throw(DomainError(dims, "The argument dims must be of non-zero length"))
+ all(>=(1), dims) || throw(DomainError(dims, "All the elements of dims must be non-zero integers (≥ 1)"))
+ (n_excitations > 0) ||
+ throw(DomainError(n_excitations, "The argument n_excitations must be a non-zero integer (≥ 1)"))
+
+ nvec = zeros(Int, L) # Vector
+ nexc = 0
+
+ # in the following, all `nvec` (Vector) will first be converted (copied) to SVector and then push to `result`
+ result = SVector{L,Int}[nvec]
+ while true
+ idx = L
+ nvec[end] += 1
+ nexc += 1
+ (nvec[idx] < dims[idx]) && push!(result, nvec)
+ while (nexc == n_excitations) || (nvec[idx] == dims[idx])
+ idx -= 1
+
+ # if idx < 1, break while-loop and return
+ if idx < 1
+ enr_size = length(result)
+ return (enr_size, Dict(zip(result, 1:enr_size)), Dict(zip(1:enr_size, result)))
+ end
+
+ nexc -= nvec[idx+1] - 1
+ nvec[idx+1] = 0
+ nvec[idx] += 1
+ (nvec[idx] < dims[idx]) && push!(result, nvec)
+ end
+ end
+end
+
+@doc raw"""
+ enr_fock(dims::Union{AbstractVector,Tuple}, n_excitations::Int, state::AbstractVector; sparse::Union{Bool,Val}=Val(false))
+ enr_fock(s_enr::EnrSpace, state::AbstractVector; sparse::Union{Bool,Val}=Val(false))
+
+Generate the Fock state representation ([`Ket`](@ref)) in an excitation number restricted state space ([`EnrSpace`](@ref)).
+
+The arguments `dims` and `n_excitations` are used to generate [`EnrSpace`](@ref).
+
+The `state` argument is a list of integers that specifies the state (in the number basis representation) for which to generate the Fock state representation.
+
+!!! warning "Beware of type-stability!"
+ It is highly recommended to use `enr_fock(dims, n_excitations, state)` with `dims` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) to keep type stability. 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 enr_fock(
+ dims::Union{AbstractVector{T},NTuple{N,T}},
+ n_excitations::Int,
+ state::AbstractVector{T};
+ sparse::Union{Bool,Val} = Val(false),
+) where {T<:Integer,N}
+ s_enr = EnrSpace(dims, n_excitations)
+ return enr_fock(s_enr, state, sparse = sparse)
+end
+function enr_fock(s_enr::EnrSpace, state::AbstractVector{T}; sparse::Union{Bool,Val} = Val(false)) where {T<:Integer}
+ if getVal(sparse)
+ array = sparsevec([s_enr.state2idx[[state...]]], [1.0 + 0im], s_enr.size)
+ else
+ j = s_enr.state2idx[state]
+ array = [i == j ? 1.0 + 0im : 0.0 + 0im for i in 1:(s_enr.size)]
+ end
+
+ return QuantumObject(array, Ket(), s_enr)
+end
+
+@doc raw"""
+ enr_thermal_dm(dims::Union{AbstractVector,Tuple}, n_excitations::Int, n::Union{Real,AbstractVector}; sparse::Union{Bool,Val}=Val(false))
+ enr_thermal_dm(s_enr::EnrSpace, n::Union{Real,AbstractVector}; sparse::Union{Bool,Val}=Val(false))
+
+Generate the thermal state (density [`Operator`](@ref)) in an excitation number restricted state space ([`EnrSpace`](@ref)).
+
+The arguments `dims` and `n_excitations` are used to generate [`EnrSpace`](@ref).
+
+The argument `n` is a list that specifies the expectation values for number of particles in each sub-system. If `n` is specified as a real number, it will apply to each sub-system.
+
+!!! warning "Beware of type-stability!"
+ It is highly recommended to use `enr_thermal_dm(dims, n_excitations, n)` with `dims` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) to keep type stability. 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 enr_thermal_dm(
+ dims::Union{AbstractVector{T1},NTuple{N,T1}},
+ n_excitations::Int,
+ n::Union{T2,AbstractVector{T2}};
+ sparse::Union{Bool,Val} = Val(false),
+) where {T1<:Integer,T2<:Real,N}
+ s_enr = EnrSpace(dims, n_excitations)
+ return enr_thermal_dm(s_enr, n; sparse = sparse)
+end
+function enr_thermal_dm(
+ s_enr::EnrSpace{N},
+ n::Union{T,AbstractVector{T}};
+ sparse::Union{Bool,Val} = Val(false),
+) where {N,T<:Real}
+ if n isa Real
+ nvec = fill(n, N)
+ else
+ (length(n) == N) || throw(ArgumentError("The length of the vector `n` should be the same as `dims`."))
+ nvec = n
+ end
+
+ D = s_enr.size
+ idx2state = s_enr.idx2state
+
+ diags = ComplexF64[prod((nvec ./ (1 .+ nvec)) .^ idx2state[idx]) for idx in 1:D]
+ diags /= sum(diags)
+
+ if getVal(sparse)
+ return QuantumObject(spdiagm(0 => diags), Operator(), s_enr)
+ else
+ return QuantumObject(diagm(0 => diags), Operator(), s_enr)
+ end
+end
+
+@doc raw"""
+ enr_destroy(dims::Union{AbstractVector,Tuple}, n_excitations::Int)
+ enr_destroy(s_enr::EnrSpace)
+
+Generate a `Tuple` of annihilation operators for each sub-system in an excitation number restricted state space ([`EnrSpace`](@ref)). Thus, the return `Tuple` will have the same length as `dims`.
+
+The arguments `dims` and `n_excitations` are used to generate [`EnrSpace`](@ref).
+
+!!! warning "Beware of type-stability!"
+ It is highly recommended to use `enr_destroy(dims, n_excitations)` with `dims` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) to keep type stability. 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 enr_destroy(dims::Union{AbstractVector{T},NTuple{N,T}}, n_excitations::Int) where {T<:Integer,N}
+ s_enr = EnrSpace(dims, n_excitations)
+ return enr_destroy(s_enr)
+end
+function enr_destroy(s_enr::EnrSpace{N}) where {N}
+ D = s_enr.size
+ idx2state = s_enr.idx2state
+ state2idx = s_enr.state2idx
+
+ I_list = [Int64[] for _ in 1:N]
+ J_list = [Int64[] for _ in 1:N]
+ V_list = [ComplexF64[] for _ in 1:N]
+
+ for (n1, state1) in idx2state
+ for (idx, s) in pairs(state1)
+ # if s > 0, the annihilation operator of mode idx has a non-zero
+ # entry with one less excitation in mode idx in the final state
+ if s > 0
+ state2 = Vector(state1)
+ state2[idx] -= 1
+ n2 = state2idx[state2]
+ push!(I_list[idx], n2)
+ push!(J_list[idx], n1)
+ push!(V_list[idx], √s)
+ end
+ end
+ end
+
+ return ntuple(i -> QuantumObject(sparse(I_list[i], J_list[i], V_list[i], D, D), Operator(), s_enr), Val(N))
+end
+
+@doc raw"""
+ enr_identity(dims::Union{AbstractVector,Tuple}, n_excitations::Int)
+ enr_identity(s_enr::EnrSpace)
+
+Generate the identity operator in an excitation number restricted state space ([`EnrSpace`](@ref)).
+
+The arguments `dims` and `n_excitations` are used to generate [`EnrSpace`](@ref).
+
+!!! warning "Beware of type-stability!"
+ It is highly recommended to use `enr_identity(dims, n_excitations)` with `dims` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) to keep type stability. 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 enr_identity(dims::Union{AbstractVector{T},NTuple{N,T}}, n_excitations::Int) where {T<:Integer,N}
+ s_enr = EnrSpace(dims, n_excitations)
+ return enr_identity(s_enr)
+end
+enr_identity(s_enr::EnrSpace) = QuantumObject(Diagonal(ones(ComplexF64, s_enr.size)), Operator(), s_enr)
diff --git a/src/qobj/functions.jl b/src/qobj/functions.jl
index 667f7930b..820c20c30 100644
--- a/src/qobj/functions.jl
+++ b/src/qobj/functions.jl
@@ -4,20 +4,20 @@ Functions which manipulates QuantumObject
export ket2dm
export expect, variance
-export sparse_to_dense, dense_to_sparse
+export to_dense, to_sparse
export vec2mat, mat2vec
@doc raw"""
ket2dm(ψ::QuantumObject)
-Transform the ket state ``\ket{\psi}`` into a pure density matrix ``\hat{\rho} = \dyad{\psi}``.
+Transform the ket state ``\ket{\psi}`` into a pure density matrix ``\hat{\rho} = |\psi\rangle\langle\psi|``.
"""
-ket2dm(ψ::QuantumObject{<:AbstractArray{T},KetQuantumObject}) where {T} = ψ * ψ'
+ket2dm(ψ::QuantumObject{Ket}) = ψ * ψ'
-ket2dm(ρ::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} = ρ
+ket2dm(ρ::QuantumObject{Operator}) = ρ
@doc raw"""
- expect(O::QuantumObject, ψ::QuantumObject)
+ expect(O::Union{AbstractQuantumObject,Vector{AbstractQuantumObject}}, ψ::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).
@@ -27,84 +27,104 @@ If `ψ` is a density matrix ([`Operator`](@ref)), the function calculates ``\tex
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 "List of observables and states"
+ The observable `O` and state `ψ` can be given as a list of [`QuantumObject`](@ref), it returns a list of expectation values. If both of them are given as a list, it returns a `Matrix` of expectation values.
+
# Examples
-```
-julia> ψ = 1 / √2 * (fock(10,2) + fock(10,4));
+```jldoctest
+julia> ψ1 = 1 / √2 * (fock(10,2) + fock(10,4));
+
+julia> ψ2 = coherent(10, 0.6 + 0.8im);
julia> a = destroy(10);
-julia> expect(a' * a, ψ) |> round
+julia> expect(a' * a, ψ1) |> round
3.0 + 0.0im
-julia> expect(Hermitian(a' * a), ψ) |> round
+julia> expect(Hermitian(a' * a), ψ1) |> round
3.0
+
+julia> round.(expect([a' * a, a' + a, a], [ψ1, ψ2]), digits = 1)
+3×2 Matrix{ComplexF64}:
+ 3.0+0.0im 1.0+0.0im
+ 0.0+0.0im 1.2-0.0im
+ 0.0+0.0im 0.6+0.8im
```
"""
-function expect(
- O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- ψ::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
-) where {T1,T2}
- return dot(ψ.data, O.data, ψ.data)
-end
-function expect(
- O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- ψ::QuantumObject{<:AbstractArray{T2},BraQuantumObject},
-) where {T1,T2}
- return expect(O, ψ')
-end
-function expect(
- O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- ρ::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
-) where {T1,T2}
- return tr(O * ρ)
+expect(O::AbstractQuantumObject{Operator}, ψ::QuantumObject{Ket}) = dot(ψ.data, O.data, ψ.data)
+expect(O::AbstractQuantumObject{Operator}, ψ::QuantumObject{Bra}) = expect(O, ψ')
+expect(O::QuantumObject{Operator}, ρ::QuantumObject{Operator}) = tr(O * ρ)
+expect(
+ O::QuantumObject{Operator,DimsType,<:Union{<:Hermitian{TF},<:Symmetric{TR}}},
+ ψ::QuantumObject{Ket},
+) where {DimsType<:AbstractDimensions,TF<:Number,TR<:Real} = real(dot(ψ.data, O.data, ψ.data))
+expect(
+ O::QuantumObject{Operator,DimsType,<:Union{<:Hermitian{TF},<:Symmetric{TR}}},
+ ψ::QuantumObject{Bra},
+) where {DimsType<:AbstractDimensions,TF<:Number,TR<:Real} = real(expect(O, ψ'))
+expect(
+ O::QuantumObject{Operator,DimsType,<:Union{<:Hermitian{TF},<:Symmetric{TR}}},
+ ρ::QuantumObject{Operator},
+) where {DimsType<:AbstractDimensions,TF<:Number,TR<:Real} = real(tr(O * ρ))
+expect(
+ O::AbstractVector{<:AbstractQuantumObject{Operator,DimsType,<:Union{<:Hermitian{TF},<:Symmetric{TR}}}},
+ ρ::QuantumObject,
+) where {DimsType<:AbstractDimensions,TF<:Number,TR<:Real} = expect.(O, Ref(ρ))
+function expect(O::AbstractVector{<:AbstractQuantumObject{Operator}}, ρ::QuantumObject)
+ result = Vector{ComplexF64}(undef, length(O))
+ result .= expect.(O, Ref(ρ))
+ return result
end
+expect(O::AbstractQuantumObject{Operator}, ρ::AbstractVector{<:QuantumObject}) = expect.(Ref(O), ρ)
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, ψ'))
+ O::AbstractVector{<:AbstractQuantumObject{Operator,DimsType,<:Union{<:Hermitian{TF},<:Symmetric{TR}}}},
+ ρ::AbstractVector{<:QuantumObject},
+) where {DimsType<:AbstractDimensions,TF<:Number,TR<:Real}
+ N_ops = length(O)
+ result = Matrix{Float64}(undef, N_ops, length(ρ))
+ for i in 1:N_ops
+ result[i, :] .= expect.(Ref(O[i]), ρ)
+ end
+ return result
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 * ρ))
+function expect(O::AbstractVector{<:AbstractQuantumObject{Operator}}, ρ::AbstractVector{<:QuantumObject})
+ N_ops = length(O)
+ result = Matrix{ComplexF64}(undef, N_ops, length(ρ))
+ for i in 1:N_ops
+ result[i, :] .= expect.(Ref(O[i]), ρ)
+ end
+ return result
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{Operator}, ψ::QuantumObject) = expect(O^2, ψ) - expect(O, ψ)^2
+variance(O::QuantumObject{Operator}, ψ::Vector{<:QuantumObject}) = expect(O^2, ψ) .- expect(O, ψ) .^ 2
@doc raw"""
- sparse_to_dense(A::QuantumObject)
+ to_dense(A::QuantumObject)
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)
-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
+to_dense(A::QuantumObject) = QuantumObject(to_dense(A.data), A.type, A.dimensions)
+to_dense(A::MT) where {MT<:AbstractSparseArray} = Array(A)
+to_dense(A::MT) where {MT<:AbstractArray} = A
-function sparse_to_dense(::Type{M}) where {M<:SparseMatrixCSC}
+to_dense(::Type{T}, A::AbstractSparseArray) where {T<:Number} = Array{T}(A)
+to_dense(::Type{T1}, A::AbstractArray{T2}) where {T1<:Number,T2<:Number} = Array{T1}(A)
+to_dense(::Type{T}, A::AbstractArray{T}) where {T<:Number} = A
+
+function to_dense(::Type{M}) where {M<:Union{Diagonal,SparseMatrixCSC}}
T = M
par = T.parameters
npar = length(par)
@@ -112,76 +132,120 @@ function sparse_to_dense(::Type{M}) where {M<:SparseMatrixCSC}
return Matrix{par[1]}
end
-sparse_to_dense(::Type{M}) where {M<:AbstractMatrix} = M
+to_dense(::Type{M}) where {M<:AbstractMatrix} = M
@doc raw"""
- dense_to_sparse(A::QuantumObject)
+ to_sparse(A::QuantumObject)
Converts a dense QuantumObject to a sparse QuantumObject.
"""
-dense_to_sparse(A::QuantumObject{<:AbstractVecOrMat}, tol::Real = 1e-10) =
- QuantumObject(dense_to_sparse(A.data, tol), A.type, A.dims)
-function dense_to_sparse(A::MT, tol::Real = 1e-10) where {MT<:AbstractMatrix}
+to_sparse(A::QuantumObject, tol::Real = 1e-10) = QuantumObject(to_sparse(A.data, tol), A.type, A.dimensions)
+function to_sparse(A::MT, tol::Real = 1e-10) where {MT<:AbstractMatrix}
idxs = findall(@. abs(A) > tol)
row_indices = getindex.(idxs, 1)
col_indices = getindex.(idxs, 2)
vals = getindex(A, idxs)
return sparse(row_indices, col_indices, vals, size(A)...)
end
-function dense_to_sparse(A::VT, tol::Real = 1e-10) where {VT<:AbstractVector}
+function to_sparse(A::VT, tol::Real = 1e-10) where {VT<:AbstractVector}
idxs = findall(@. abs(A) > tol)
vals = getindex(A, idxs)
return sparsevec(idxs, vals, length(A))
end
@doc raw"""
- kron(A::QuantumObject, B::QuantumObject, ...)
+ kron(A::AbstractQuantumObject, B::AbstractQuantumObject, ...)
+ tensor(A::AbstractQuantumObject, B::AbstractQuantumObject, ...)
+ ⊗(A::AbstractQuantumObject, B::AbstractQuantumObject, ...)
+ A ⊗ B
Returns the [Kronecker product](https://en.wikipedia.org/wiki/Kronecker_product) ``\hat{A} \otimes \hat{B} \otimes \cdots``.
+!!! note
+ `tensor` and `⊗` (where `⊗` can be typed by tab-completing `\otimes` in the REPL) are synonyms of `kron`.
+
# Examples
-```
+```jldoctest
julia> a = destroy(20)
-Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false
+
+Quantum Object: type=Operator() dims=[20] size=(20, 20) ishermitian=false
20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries:
-⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢
-
-julia> kron(a, a)
-Quantum Object: type=Operator dims=[20, 20] size=(400, 400) ishermitian=false
-400×400 SparseMatrixCSC{ComplexF64, Int64} with 361 stored entries:
-⠀⠀⠘⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠦
+⎡⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⎦
+
+julia> O = kron(a, a);
+
+julia> size(a), size(O)
+((20, 20), (400, 400))
+
+julia> a.dims, O.dims
+([20], [20, 20])
```
"""
-function 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))
+function Base.kron(
+ A::AbstractQuantumObject{OpType,<:Dimensions},
+ B::AbstractQuantumObject{OpType,<:Dimensions},
+) where {OpType<:Union{Ket,Bra,Operator}}
+ QType = promote_op_type(A, B)
+ _lazy_tensor_warning(A.data, B.data)
+ return QType(kron(A.data, B.data), A.type, Dimensions((A.dimensions.to..., B.dimensions.to...)))
+end
+
+# if A and B are both Operator but either one of them has GeneralDimensions
+for ADimType in (:Dimensions, :GeneralDimensions)
+ for BDimType in (:Dimensions, :GeneralDimensions)
+ if !(ADimType == BDimType == :Dimensions) # not for this case because it's already implemented
+ @eval begin
+ function Base.kron(
+ A::AbstractQuantumObject{Operator,<:$ADimType},
+ B::AbstractQuantumObject{Operator,<:$BDimType},
+ )
+ QType = promote_op_type(A, B)
+ _lazy_tensor_warning(A.data, B.data)
+ return QType(
+ kron(A.data, B.data),
+ Operator(),
+ GeneralDimensions(
+ (get_dimensions_to(A)..., get_dimensions_to(B)...),
+ (get_dimensions_from(A)..., get_dimensions_from(B)...),
+ ),
+ )
+ end
+ end
+ end
+ end
+end
+
+# if A and B are different type (must return Operator with GeneralDimensions)
+for AOpType in (:Ket, :Bra, :Operator)
+ for BOpType in (:Ket, :Bra, :Operator)
+ if (AOpType != BOpType)
+ @eval begin
+ function Base.kron(A::AbstractQuantumObject{$AOpType}, B::AbstractQuantumObject{$BOpType})
+ QType = promote_op_type(A, B)
+ _lazy_tensor_warning(A.data, B.data)
+ return QType(
+ kron(A.data, B.data),
+ Operator(),
+ GeneralDimensions(
+ (get_dimensions_to(A)..., get_dimensions_to(B)...),
+ (get_dimensions_from(A)..., get_dimensions_from(B)...),
+ ),
+ )
+ end
+ end
+ end
+ end
+end
+
+Base.kron(A::AbstractQuantumObject) = A
+function Base.kron(A::Vector{<:AbstractQuantumObject})
+ @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"""
@@ -196,19 +260,25 @@ end
@doc raw"""
vec2mat(A::QuantumObject)
+ vector_to_operator(A::QuantumObject)
-Convert a quantum object from vector ([`OperatorKetQuantumObject`](@ref)-type) to matrix ([`OperatorQuantumObject`](@ref)-type)
+Convert a quantum object from vector ([`OperatorKet`](@ref)-type) to matrix ([`Operator`](@ref)-type)
+
+!!! note
+ `vector_to_operator` is a synonym of `vec2mat`.
"""
-vec2mat(A::QuantumObject{<:AbstractArray{T},OperatorKetQuantumObject}) where {T} =
- QuantumObject(vec2mat(A.data), Operator, A.dims)
+vec2mat(A::QuantumObject{OperatorKet}) = QuantumObject(vec2mat(A.data), Operator(), A.dimensions)
@doc raw"""
mat2vec(A::QuantumObject)
+ operator_to_vector(A::QuantumObject)
+
+Convert a quantum object from matrix ([`Operator`](@ref)-type) to vector ([`OperatorKet`](@ref)-type)
-Convert a quantum object from matrix ([`OperatorQuantumObject`](@ref)-type) to vector ([`OperatorKetQuantumObject`](@ref)-type)
+!!! note
+ `operator_to_vector` is a synonym of `mat2vec`.
"""
-mat2vec(A::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} =
- QuantumObject(mat2vec(A.data), OperatorKet, A.dims)
+mat2vec(A::QuantumObject{Operator}) = QuantumObject(mat2vec(A.data), OperatorKet(), A.dimensions)
@doc raw"""
mat2vec(A::AbstractMatrix)
diff --git a/src/qobj/operator_sum.jl b/src/qobj/operator_sum.jl
deleted file mode 100644
index 6f3642f5e..000000000
--- a/src/qobj/operator_sum.jl
+++ /dev/null
@@ -1,49 +0,0 @@
-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``.
-
-This is very useful when we have to update only the coefficients, without allocating memory by performing the sum of the operators.
-"""
-struct OperatorSum{CT<:Vector{<:Number},OT<:AbstractVector} <: AbstractQuantumObject
- coefficients::CT
- operators::OT
- function OperatorSum(coefficients::CT, operators::OT) where {CT<:Vector{<:Number},OT<:AbstractVector}
- length(coefficients) == length(operators) ||
- throw(DimensionMismatch("The number of coefficients must be the same as the number of operators."))
- # Check if all the operators have the same dimensions
- size_1 = size(operators[1])
- 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(eltype, promote_type, coefficients),
- )
- coefficients2 = T.(coefficients)
- return new{Vector{T},OT}(coefficients2, operators)
- end
-end
-
-Base.size(A::OperatorSum) = size(A.operators[1])
-Base.size(A::OperatorSum, inds...) = size(A.operators[1], inds...)
-Base.length(A::OperatorSum) = length(A.operators[1])
-Base.copy(A::OperatorSum) = OperatorSum(copy(A.coefficients), copy(A.operators))
-Base.deepcopy(A::OperatorSum) = OperatorSum(deepcopy(A.coefficients), deepcopy(A.operators))
-
-function update_coefficients!(A::OperatorSum, coefficients)
- length(A.coefficients) == length(coefficients) ||
- throw(DimensionMismatch("The number of coefficients must be the same as the number of operators."))
- return A.coefficients .= coefficients
-end
-
-@inline function LinearAlgebra.mul!(y::AbstractVector{T}, A::OperatorSum, x::AbstractVector, α, β) where {T}
- # Note that β is applied only to the first term
- mul!(y, A.operators[1], x, α * A.coefficients[1], β)
- @inbounds for i in 2:length(A.operators)
- A.coefficients[i] == 0 && continue
- mul!(y, A.operators[i], x, α * A.coefficients[i], 1)
- end
- return y
-end
diff --git a/src/qobj/operators.jl b/src/qobj/operators.jl
index cf934fe5e..fb0c2041f 100644
--- a/src/qobj/operators.jl
+++ b/src/qobj/operators.jl
@@ -19,23 +19,23 @@ 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::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems.
+- `dimensions::Union{Dimensions,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.
+ 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` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl). 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::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::Union{Dimensions,AbstractVector{Int},Tuple}, distribution::Union{Symbol,Val} = Val(:haar)) =
rand_unitary(dimensions, makeVal(distribution))
-function rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{:haar})
+function rand_unitary(dimensions::Union{Dimensions,AbstractVector{Int},Tuple}, ::Val{:haar})
N = prod(dimensions)
# generate N x N matrix Z of complex standard normal random variates
@@ -48,36 +48,32 @@ function rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::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(to_dense(Q * Diagonal(Λ)); type = Operator(), dims = dimensions)
end
-function rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{:exp})
+function rand_unitary(dimensions::Union{Dimensions,AbstractVector{Int},Tuple}, ::Val{:exp})
N = prod(dimensions)
# generate N x N matrix Z of complex standard normal random variates
Z = randn(ComplexF64, N, N)
# generate Hermitian matrix
- H = QuantumObject((Z + Z') / 2; type = Operator, dims = dimensions)
+ H = QuantumObject((Z + Z') / 2; type = Operator(), dims = dimensions)
- return exp(-1.0im * H)
+ return to_dense(exp(-1.0im * H))
end
-rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{T}) where {T} =
+rand_unitary(dimensions::Union{Dimensions,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)
"""
-commutator(
- A::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject};
- anti::Bool = false,
-) where {T1,T2} = A * B - (-1)^anti * B * A
+commutator(A::QuantumObject{Operator}, B::QuantumObject{Operator}; anti::Bool = false) = A * B - (-1)^anti * B * A
@doc raw"""
destroy(N::Int)
@@ -88,9 +84,10 @@ This operator acts on a fock state as ``\hat{a} \ket{n} = \sqrt{n} \ket{n-1}``.
# Examples
-```
+```jldoctest
julia> a = destroy(20)
-Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false
+
+Quantum Object: type=Operator() dims=[20] size=(20, 20) ishermitian=false
20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries:
⎡⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⎥
@@ -102,7 +99,7 @@ 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)
@@ -113,9 +110,10 @@ This operator acts on a fock state as ``\hat{a}^\dagger \ket{n} = \sqrt{n+1} \ke
# Examples
-```
+```jldoctest
julia> a_d = create(20)
-Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false
+
+Quantum Object: type=Operator() dims=[20] size=(20, 20) ishermitian=false
20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries:
⎡⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⎥
@@ -127,7 +125,7 @@ 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)
@@ -168,7 +166,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)
@@ -224,7 +222,7 @@ 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"""
@@ -233,30 +231,33 @@ end
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
-```
+```jldoctest
julia> jmat(0.5, :x)
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
+
+Quantum Object: type=Operator() dims=[2] size=(2, 2) ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
⋅ 0.5+0.0im
0.5+0.0im ⋅
-julia> jmat(0.5, :-)
-Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=false
+julia> jmat(0.5, Val(:-))
+
+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
+
+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 ⋅ ⋅
@@ -275,7 +276,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
@@ -283,42 +284,42 @@ 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)"))
function _jm(j::Real)
- m = j:(-1):-j
- data = sqrt.(j * (j + 1) .- m .* (m .- 1))[1:end-1]
+ m = j:(-1):(-j)
+ data = sqrt.(j * (j + 1) .- m .* (m .- 1))[1:(end-1)]
return spdiagm(-1 => Array{ComplexF64}(data))
end
-_jz(j::Real) = spdiagm(0 => Array{ComplexF64}(j .- (0:Int(2 * j))))
+_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).
"""
@@ -327,7 +328,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).
"""
@@ -336,7 +337,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).
"""
@@ -345,7 +346,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).
"""
@@ -354,7 +355,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).
"""
@@ -363,7 +364,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).
"""
@@ -372,7 +373,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).
"""
@@ -381,7 +382,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).
"""
@@ -408,7 +409,7 @@ sigmay() = rmul!(jmat(0.5, Val(:y)), 2)
@doc raw"""
sigmaz()
-Pauli operator ``\hat{\sigma}_z = \comm{\hat{\sigma}_+}{\hat{\sigma}_-}``.
+Pauli operator ``\hat{\sigma}_z = \left[ \hat{\sigma}_+ , \hat{\sigma}_- \right]``.
See also [`jmat`](@ref).
"""
@@ -416,19 +417,23 @@ sigmaz() = rmul!(jmat(0.5, Val(:z)), 2)
@doc raw"""
eye(N::Int; type=Operator, dims=nothing)
+ qeye(N::Int; type=Operator, dims=nothing)
Identity operator ``\hat{\mathbb{1}}`` with size `N`.
It is also possible to specify the list of Hilbert dimensions `dims` if different subsystems are present.
Note that `type` can only be either [`Operator`](@ref) or [`SuperOperator`](@ref)
+
+!!! note
+ `qeye` is a synonym of `eye`.
"""
-eye(
- N::Int;
- type::ObjType = Operator,
- dims = nothing,
-) where {ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(Diagonal(ones(ComplexF64, N)); type = type, dims = dims)
+function eye(N::Int; type = Operator(), dims = nothing)
+ if dims isa Nothing
+ dims = isa(type, Operator) ? N : isqrt(N)
+ end
+ return QuantumObject(Diagonal(ones(ComplexF64, N)); type = type, dims = dims)
+end
@doc raw"""
fdestroy(N::Union{Int,Val}, j::Int)
@@ -437,15 +442,17 @@ Construct a fermionic destruction operator acting on the `j`-th site, where the
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::Union{Int,Val}, j::Int) = _Jordan_Wigner(N, j, sigmam())
+fdestroy(N::Union{Int,Val}, j::Int) = _Jordan_Wigner(N, j, sigmap())
@doc raw"""
fcreate(N::Union{Int,Val}, j::Int)
@@ -454,38 +461,40 @@ Construct a fermionic creation operator acting on the `j`-th site, where the foc
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::Union{Int,Val}, 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)
+_Jordan_Wigner(N::Int, j::Int, op::QuantumObject{Operator}) = _Jordan_Wigner(Val(N), j, op)
-function _Jordan_Wigner(::Val{N}, j::Int, op::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {N,T}
+function _Jordan_Wigner(::Val{N}, j::Int, op::QuantumObject{Operator}) where {N}
(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 = ntuple(i -> 2, Val(N)))
+ return QuantumObject(kron(Z_tensor, op.data, I_tensor); type = Operator(), dims = ntuple(i -> 2, Val(N)))
end
@doc raw"""
projection(N::Int, i::Int, j::Int)
-Generates the projection operator ``\hat{O} = \dyad{i}{j}`` with Hilbert space dimension `N`.
+Generates the projection operator ``\hat{O} = |i \rangle\langle 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), type = Operator)
+projection(N::Int, i::Int, j::Int) =
+ QuantumObject(sparse([i + 1], [j + 1], [1.0 + 0.0im], N, N), type = Operator(), dims = N)
@doc raw"""
tunneling(N::Int, m::Int=1; sparse::Union{Bool,Val{<:Bool}}=Val(false))
@@ -507,10 +516,10 @@ 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 getVal(makeVal(sparse))
- return QuantumObject(spdiagm(m => data, -m => data); type = Operator, dims = N)
+ if getVal(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
@@ -521,7 +530,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::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems.
+- `dimensions::Union{Dimensions,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
@@ -539,11 +548,11 @@ 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.
+ It is highly recommended to use `qft(dimensions)` with `dimensions` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) 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::Union{AbstractVector{T},Tuple}) where {T} =
- QuantumObject(_qft_op(prod(dimensions)), Operator, dimensions)
+qft(dimensions::Int) = QuantumObject(_qft_op(dimensions), Operator(), dimensions)
+qft(dimensions::Union{Dimensions,AbstractVector{Int},Tuple}) =
+ 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 1420cc70a..053b9b050 100644
--- a/src/qobj/quantum_object.jl
+++ b/src/qobj/quantum_object.jl
@@ -1,129 +1,31 @@
#=
-This file defines:
- 1. the QuantumObject (Qobj) structure
- 2. all the type structures for QuantumObject
-Also support for fundamental functions in Julia standard library:
- - Base: show, length, size, eltype, getindex, setindex!, isequal, :(==), isapprox, Vector, Matrix
+This file defines the QuantumObject (Qobj) structure.
+It also implements the fundamental functions in Julia standard library:
+ - Base: show, real, imag, Vector, Matrix
- SparseArrays: sparse, nnz, nonzeros, rowvals, droptol!, dropzeros, dropzeros!, SparseVector, SparseMatrixCSC
+ - SciMLOperators: cache_operator
=#
-export AbstractQuantumObject, QuantumObject
-export QuantumObjectType,
- BraQuantumObject,
- KetQuantumObject,
- OperatorQuantumObject,
- OperatorBraQuantumObject,
- OperatorKetQuantumObject,
- SuperOperatorQuantumObject
-export Bra, Ket, Operator, OperatorBra, OperatorKet, SuperOperator
-
-abstract type AbstractQuantumObject end
-abstract type QuantumObjectType end
-
-@doc raw"""
- BraQuantumObject <: QuantumObjectType
-
-Constructor representing a bra state ``\langle\psi|``.
-"""
-struct BraQuantumObject <: QuantumObjectType end
-Base.show(io::IO, ::BraQuantumObject) = print(io, "Bra")
-
-@doc raw"""
- const Bra = BraQuantumObject()
-
-A constant representing the type of [`BraQuantumObject`](@ref): a bra state ``\langle\psi|``
-"""
-const Bra = BraQuantumObject()
-
-@doc raw"""
- KetQuantumObject <: QuantumObjectType
-
-Constructor representing a ket state ``|\psi\rangle``.
-"""
-struct KetQuantumObject <: QuantumObjectType end
-Base.show(io::IO, ::KetQuantumObject) = print(io, "Ket")
-
-@doc raw"""
- const Ket = KetQuantumObject()
-
-A constant representing the type of [`KetQuantumObject`](@ref): a ket state ``|\psi\rangle``
-"""
-const Ket = KetQuantumObject()
-
-@doc raw"""
- OperatorQuantumObject <: QuantumObjectType
-
-Constructor representing an operator ``\hat{O}``.
-"""
-struct OperatorQuantumObject <: QuantumObjectType end
-Base.show(io::IO, ::OperatorQuantumObject) = print(io, "Operator")
-
-@doc raw"""
- const Operator = OperatorQuantumObject()
-
-A constant representing the type of [`OperatorQuantumObject`](@ref): an operator ``\hat{O}``
-"""
-const Operator = OperatorQuantumObject()
-
-@doc raw"""
- SuperOperatorQuantumObject <: QuantumObjectType
-
-Constructor representing a super-operator ``\hat{\mathcal{O}}`` acting on vectorized density operator matrices.
-"""
-struct SuperOperatorQuantumObject <: QuantumObjectType end
-Base.show(io::IO, ::SuperOperatorQuantumObject) = print(io, "SuperOperator")
-
-@doc raw"""
- const SuperOperator = SuperOperatorQuantumObject()
-
-A constant representing the type of [`SuperOperatorQuantumObject`](@ref): a super-operator ``\hat{\mathcal{O}}`` acting on vectorized density operator matrices
-"""
-const SuperOperator = SuperOperatorQuantumObject()
-
-@doc raw"""
- OperatorBraQuantumObject <: QuantumObjectType
-
-Constructor representing a bra state in the [`SuperOperator`](@ref) formalism ``\langle\langle\rho|``.
-"""
-struct OperatorBraQuantumObject <: QuantumObjectType end
-Base.show(io::IO, ::OperatorBraQuantumObject) = print(io, "OperatorBra")
-
-@doc raw"""
- const OperatorBra = OperatorBraQuantumObject()
-
-A constant representing the type of [`OperatorBraQuantumObject`](@ref): a bra state in the [`SuperOperator`](@ref) formalism ``\langle\langle\rho|``.
-"""
-const OperatorBra = OperatorBraQuantumObject()
-
-@doc raw"""
- OperatorKetQuantumObject <: QuantumObjectType
-
-Constructor representing a ket state in the [`SuperOperator`](@ref) formalism ``|\rho\rangle\rangle``.
-"""
-struct OperatorKetQuantumObject <: QuantumObjectType end
-Base.show(io::IO, ::OperatorKetQuantumObject) = print(io, "OperatorKet")
+export QuantumObject
@doc raw"""
- const OperatorKet = OperatorKetQuantumObject()
-
-A constant representing the type of [`OperatorKetQuantumObject`](@ref): a ket state in the [`SuperOperator`](@ref) formalism ``|\rho\rangle\rangle``
-"""
-const OperatorKet = OperatorKetQuantumObject()
-
-@doc raw"""
- struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType,N}
- data::MT
+ struct QuantumObject{ObjType<:QuantumObjectType,DimType<:AbstractDimensions,DataType<:AbstractArray} <: AbstractQuantumObject{ObjType,DimType,DataType}
+ data::DataType
type::ObjType
- dims::SVector{N, Int}
+ dimensions::DimType
end
-Julia struct representing any quantum objects.
+Julia structure representing any time-independent quantum objects. For time-dependent cases, see [`QuantumObjectEvolution`](@ref).
+
+!!! note "`dims` property"
+ For a given `H::QuantumObject`, `H.dims` or `getproperty(H, :dims)` returns its `dimensions` in the type of integer-vector.
# Examples
-```
+```jldoctest
julia> a = destroy(20)
-Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false
+
+Quantum Object: type=Operator() dims=[20] size=(20, 20) ishermitian=false
20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries:
⎡⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⎥
@@ -133,186 +35,129 @@ Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false
julia> a isa QuantumObject
true
+
+julia> a.dims
+1-element StaticArraysCore.SVector{1, Int64} with indices SOneTo(1):
+ 20
+
+julia> a.dimensions
+Dimensions{1, Tuple{Space}}((Space(20),))
```
"""
-struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType,N} <: AbstractQuantumObject
- data::MT
+struct QuantumObject{ObjType<:QuantumObjectType,DimType<:AbstractDimensions,DataType<:AbstractArray} <:
+ AbstractQuantumObject{ObjType,DimType,DataType}
+ data::DataType
type::ObjType
- dims::SVector{N,Int}
+ dimensions::DimType
- function QuantumObject(data::MT, type::ObjType, dims) where {MT<:AbstractArray,ObjType<:QuantumObjectType}
- _check_dims(dims)
+ function QuantumObject(data::DT, type, dims) where {DT<:AbstractArray}
+ dimensions = _gen_dimensions(dims)
- _size = _get_size(data)
- _check_QuantumObject(type, dims, _size[1], _size[2])
+ ObjType = _check_type(type)
- N = length(dims)
+ _size = _get_size(data)
+ _check_QuantumObject(type, dimensions, _size[1], _size[2])
- return new{MT,ObjType,N}(data, type, SVector{N,Int}(dims))
+ return new{ObjType,typeof(dimensions),DT}(data, type, dimensions)
end
end
-function QuantumObject(A::AbstractArray, type::ObjType, dims::Integer) where {ObjType<:QuantumObjectType}
- return QuantumObject(A, type, SVector{1,Int}(dims))
-end
+@doc raw"""
+ Qobj(A::AbstractArray; type = nothing, dims = nothing)
+ QuantumObject(A::AbstractArray; type = nothing, dims = nothing)
+
+Generate [`QuantumObject`](@ref) with a given `A::AbstractArray` and specified `type::QuantumObjectType` and `dims`.
-function QuantumObject(
- A::AbstractMatrix{T};
- type::ObjType = nothing,
- dims = nothing,
-) where {T,ObjType<:Union{Nothing,QuantumObjectType}}
+!!! note
+ `Qobj` is a synonym of `QuantumObject`.
+"""
+function QuantumObject(A::AbstractMatrix{T}; type = nothing, dims = nothing) where {T}
_size = _get_size(A)
+ _check_type(type)
+
if type isa Nothing
- type = (_size[1] == 1 && _size[2] > 1) ? Bra : Operator # default type
- elseif type != Operator && type != SuperOperator && type != Bra && type != OperatorBra
+ type = (_size[1] == 1 && _size[2] > 1) ? Bra() : Operator() # default type
+ elseif !(type isa Operator) && !(type isa SuperOperator) && !(type isa Bra) && !(type isa OperatorBra)
throw(
ArgumentError(
- "The argument type must be Operator, SuperOperator, Bra or OperatorBra if the input array is a matrix.",
+ "The argument type must be Operator(), SuperOperator(), Bra() or OperatorBra() if the input array is a matrix.",
),
)
end
if dims isa Nothing
- if type isa OperatorQuantumObject || type isa BraQuantumObject
- dims = SVector{1,Int}(_size[2])
- elseif type isa SuperOperatorQuantumObject || type isa OperatorBraQuantumObject
- dims = SVector{1,Int}(isqrt(_size[2]))
+ if type isa Bra
+ dims = Dimensions(_size[2])
+ elseif type isa Operator
+ dims =
+ (_size[1] == _size[2]) ? Dimensions(_size[1]) :
+ GeneralDimensions(SVector{2}(SVector{1}(_size[1]), SVector{1}(_size[2])))
+ elseif type isa SuperOperator || type isa OperatorBra
+ dims = Dimensions(isqrt(_size[2]))
end
end
return QuantumObject(A, type, dims)
end
-function QuantumObject(
- A::AbstractVector{T};
- type::ObjType = nothing,
- dims = nothing,
-) where {T,ObjType<:Union{Nothing,QuantumObjectType}}
+function QuantumObject(A::AbstractVector{T}; type = nothing, dims = nothing) where {T}
+ _check_type(type)
if type isa Nothing
- type = Ket # default type
- elseif type != Ket && type != OperatorKet
- throw(ArgumentError("The argument type must be Ket or OperatorKet if the input array is a vector."))
+ type = Ket() # default type
+ elseif !(type isa Ket) && !(type isa OperatorKet)
+ throw(ArgumentError("The argument type must be Ket() or OperatorKet() if the input array is a vector."))
end
if dims isa Nothing
_size = _get_size(A)
- if type isa KetQuantumObject
- dims = SVector{1,Int}(_size[1])
- elseif type isa OperatorKetQuantumObject
- dims = SVector{1,Int}(isqrt(_size[1]))
+ if type isa Ket
+ dims = Dimensions(_size[1])
+ elseif type isa OperatorKet
+ dims = Dimensions(isqrt(_size[1]))
end
end
return QuantumObject(A, type, dims)
end
-function QuantumObject(
- A::AbstractArray{T,N};
- type::ObjType = nothing,
- dims = nothing,
-) where {T,N,ObjType<:Union{Nothing,QuantumObjectType}}
+function QuantumObject(A::AbstractArray{T,N}; type = nothing, dims = nothing) where {T,N}
throw(DomainError(size(A), "The size of the array is not compatible with vector or matrix."))
end
-_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
-
-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, 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, 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, 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, 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, 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, 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))."))
- return nothing
-end
-
-function QuantumObject(
- A::QuantumObject{<:AbstractArray{T,N}};
- type::ObjType = A.type,
- dims = A.dims,
-) where {T,N,ObjType<:QuantumObjectType}
- _size = N == 1 ? (length(A), 1) : size(A)
- _check_QuantumObject(type, dims, _size[1], _size[2])
- return QuantumObject(copy(A.data), type, dims)
+function QuantumObject(A::QuantumObject; type = A.type, dims = A.dimensions)
+ _size = _get_size(A.data)
+ dimensions = _gen_dimensions(dims)
+ _check_type(type)
+ _check_QuantumObject(type, dimensions, _size[1], _size[2])
+ return QuantumObject(copy(A.data), type, dimensions)
end
function Base.show(
io::IO,
- QO::QuantumObject{<:AbstractArray{T},OpType},
-) where {
- T,
- OpType<:Union{
- BraQuantumObject,
- KetQuantumObject,
- OperatorBraQuantumObject,
- OperatorKetQuantumObject,
- SuperOperatorQuantumObject,
- },
-}
+ QO::QuantumObject{OpType},
+) where {OpType<:Union{Bra,Ket,OperatorBra,OperatorKet,SuperOperator}}
op_data = QO.data
- println(io, "Quantum Object: type=", QO.type, " dims=", QO.dims, " size=", size(op_data))
+ println(
+ io,
+ "\nQuantum Object: type=",
+ QO.type,
+ " dims=",
+ _get_dims_string(QO.dimensions),
+ " size=",
+ size(op_data),
+ )
return show(io, MIME("text/plain"), op_data)
end
-function Base.show(io::IO, QO::QuantumObject{<:AbstractArray{T},OpType}) where {T,OpType<:OperatorQuantumObject}
+function Base.show(io::IO, QO::QuantumObject)
op_data = QO.data
println(
io,
- "Quantum Object: type=",
+ "\nQuantum Object: type=",
QO.type,
" dims=",
- QO.dims,
+ _get_dims_string(QO.dimensions),
" size=",
size(op_data),
" ishermitian=",
@@ -321,61 +166,55 @@ function Base.show(io::IO, QO::QuantumObject{<:AbstractArray{T},OpType}) where {
return show(io, MIME("text/plain"), op_data)
end
-@doc raw"""
- size(A::QuantumObject)
- size(A::QuantumObject, idx::Int)
-
-Returns a tuple containing each dimensions of the array in the [`QuantumObject`](@ref).
-
-Optionally, you can specify an index (`idx`) to just get the corresponding dimension of the array.
-"""
-Base.size(A::QuantumObject{<:AbstractArray{T}}) where {T} = size(A.data)
-Base.size(A::QuantumObject{<:AbstractArray{T}}, idx::Int) where {T} = size(A.data, idx)
+Base.real(x::QuantumObject) = QuantumObject(real(x.data), x.type, x.dimensions)
+Base.imag(x::QuantumObject) = QuantumObject(imag(x.data), x.type, x.dimensions)
-Base.getindex(A::QuantumObject{<:AbstractArray{T}}, inds...) where {T} = getindex(A.data, inds...)
-Base.setindex!(A::QuantumObject{<:AbstractArray{T}}, val, inds...) where {T} = setindex!(A.data, val, inds...)
+SparseArrays.sparse(A::QuantumObject) = QuantumObject(sparse(A.data), A.type, A.dimensions)
+SparseArrays.nnz(A::QuantumObject) = nnz(A.data)
+SparseArrays.nonzeros(A::QuantumObject) = nonzeros(A.data)
+SparseArrays.rowvals(A::QuantumObject) = rowvals(A.data)
+SparseArrays.droptol!(A::QuantumObject, tol::Real) = (droptol!(A.data, tol); return A)
+SparseArrays.dropzeros(A::QuantumObject) = QuantumObject(dropzeros(A.data), A.type, A.dimensions)
+SparseArrays.dropzeros!(A::QuantumObject) = (dropzeros!(A.data); return A)
@doc raw"""
- eltype(A::QuantumObject)
+ SciMLOperators.cached_operator(L::AbstractQuantumObject, u)
-Returns the elements type of the matrix or vector corresponding to the [`QuantumObject`](@ref) `A`.
+Allocate caches for [`AbstractQuantumObject`](@ref) `L` for in-place evaluation with `u`-like input vectors.
+
+Here, `u` can be in either the following types:
+- `AbstractVector`
+- [`Ket`](@ref)-type [`QuantumObject`](@ref) (if `L` is an [`Operator`](@ref))
+- [`OperatorKet`](@ref)-type [`QuantumObject`](@ref) (if `L` is a [`SuperOperator`](@ref))
"""
-Base.eltype(A::QuantumObject) = eltype(A.data)
+SciMLOperators.cache_operator(
+ L::AbstractQuantumObject{OpType},
+ u::AbstractVector,
+) where {OpType<:Union{Operator,SuperOperator}} =
+ get_typename_wrapper(L)(cache_operator(L.data, to_dense(similar(u))), L.type, L.dimensions)
-@doc raw"""
- length(A::QuantumObject)
+function SciMLOperators.cache_operator(
+ L::AbstractQuantumObject{OpType},
+ u::QuantumObject{SType},
+) where {OpType<:Union{Operator,SuperOperator},SType<:Union{Ket,OperatorKet}}
+ check_dimensions(L, u)
-Returns the length of the matrix or vector corresponding to the [`QuantumObject`](@ref) `A`.
-"""
-Base.length(A::QuantumObject{<:AbstractArray{T}}) where {T} = length(A.data)
-
-Base.isequal(A::QuantumObject{<:AbstractArray{T}}, B::QuantumObject{<:AbstractArray{T}}) where {T} =
- isequal(A.type, B.type) && isequal(A.dims, B.dims) && isequal(A.data, B.data)
-Base.isapprox(A::QuantumObject{<:AbstractArray{T}}, B::QuantumObject{<:AbstractArray{T}}; kwargs...) where {T} =
- isequal(A.type, B.type) && isequal(A.dims, B.dims) && isapprox(A.data, B.data; kwargs...)
-Base.:(==)(A::QuantumObject{<:AbstractArray{T}}, B::QuantumObject{<:AbstractArray{T}}) where {T} =
- (A.type == B.type) && (A.dims == B.dims) && (A.data == B.data)
-
-Base.real(x::QuantumObject) = QuantumObject(real(x.data), x.type, x.dims)
-Base.imag(x::QuantumObject) = QuantumObject(imag(x.data), x.type, x.dims)
-
-SparseArrays.sparse(A::QuantumObject{<:AbstractArray{T}}) where {T} = QuantumObject(sparse(A.data), A.type, A.dims)
-SparseArrays.nnz(A::QuantumObject{<:AbstractSparseArray}) = nnz(A.data)
-SparseArrays.nonzeros(A::QuantumObject{<:AbstractSparseArray}) = nonzeros(A.data)
-SparseArrays.rowvals(A::QuantumObject{<:AbstractSparseArray}) = rowvals(A.data)
-SparseArrays.droptol!(A::QuantumObject{<:AbstractSparseArray}, tol::Real) = (droptol!(A.data, tol); return A)
-SparseArrays.dropzeros(A::QuantumObject{<:AbstractSparseArray}) = QuantumObject(dropzeros(A.data), A.type, A.dims)
-SparseArrays.dropzeros!(A::QuantumObject{<:AbstractSparseArray}) = (dropzeros!(A.data); return A)
+ if isoper(L) && isoperket(u)
+ throw(ArgumentError("The input state `u` must be a Ket if `L` is an Operator."))
+ elseif issuper(L) && isket(u)
+ throw(ArgumentError("The input state `u` must be an OperatorKet if `L` is a SuperOperator."))
+ end
+ return cache_operator(L, u.data)
+end
# data type conversions
-Base.Vector(A::QuantumObject{<:AbstractVector}) = QuantumObject(Vector(A.data), A.type, A.dims)
-Base.Vector{T}(A::QuantumObject{<:AbstractVector}) where {T<:Number} = QuantumObject(Vector{T}(A.data), A.type, A.dims)
-Base.Matrix(A::QuantumObject{<:AbstractMatrix}) = QuantumObject(Matrix(A.data), A.type, A.dims)
-Base.Matrix{T}(A::QuantumObject{<:AbstractMatrix}) where {T<:Number} = QuantumObject(Matrix{T}(A.data), A.type, A.dims)
-SparseArrays.SparseVector(A::QuantumObject{<:AbstractVector}) = QuantumObject(SparseVector(A.data), A.type, A.dims)
-SparseArrays.SparseVector{T}(A::QuantumObject{<:SparseVector}) where {T<:Number} =
- QuantumObject(SparseVector{T}(A.data), A.type, A.dims)
-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)
+Base.Vector(A::QuantumObject) = QuantumObject(Vector(A.data), A.type, A.dimensions)
+Base.Vector{T}(A::QuantumObject) where {T<:Number} = QuantumObject(Vector{T}(A.data), A.type, A.dimensions)
+Base.Matrix(A::QuantumObject) = QuantumObject(Matrix(A.data), A.type, A.dimensions)
+Base.Matrix{T}(A::QuantumObject) where {T<:Number} = QuantumObject(Matrix{T}(A.data), A.type, A.dimensions)
+SparseArrays.SparseVector(A::QuantumObject) = QuantumObject(SparseVector(A.data), A.type, A.dimensions)
+SparseArrays.SparseVector{T}(A::QuantumObject) where {T<:Number} =
+ QuantumObject(SparseVector{T}(A.data), A.type, A.dimensions)
+SparseArrays.SparseMatrixCSC(A::QuantumObject) = QuantumObject(SparseMatrixCSC(A.data), A.type, A.dimensions)
+SparseArrays.SparseMatrixCSC{T}(A::QuantumObject) where {T<:Number} =
+ QuantumObject(SparseMatrixCSC{T}(A.data), A.type, A.dimensions)
diff --git a/src/qobj/quantum_object_base.jl b/src/qobj/quantum_object_base.jl
new file mode 100644
index 000000000..6ae1024d3
--- /dev/null
+++ b/src/qobj/quantum_object_base.jl
@@ -0,0 +1,249 @@
+#=
+This file defines the AbstractQuantumObject structure, all the type structures for AbstractQuantumObject, and fundamental functions in Julia standard library:
+ - Base: show, length, size, copy, eltype, getindex, setindex!, isequal, :(==), isapprox
+=#
+
+export AbstractQuantumObject
+export QuantumObjectType, SuperOperatorType, Bra, Ket, Operator, OperatorBra, OperatorKet, SuperOperator
+
+@doc raw"""
+ abstract type AbstractQuantumObject{ObjType,DimType,DataType}
+
+Abstract type for all quantum objects like [`QuantumObject`](@ref) and [`QuantumObjectEvolution`](@ref).
+
+# Example
+```jldoctest
+julia> sigmax() isa AbstractQuantumObject
+true
+```
+"""
+abstract type AbstractQuantumObject{ObjType,DimType,DataType} end
+
+abstract type QuantumObjectType end
+
+abstract type SuperOperatorType <: QuantumObjectType end
+
+@doc raw"""
+ Bra <: QuantumObjectType
+
+Constructor representing a bra state ``\langle\psi|``.
+"""
+struct Bra <: QuantumObjectType end
+
+Base.show(io::IO, ::Bra) = print(io, "Bra()")
+
+@doc raw"""
+ Ket <: QuantumObjectType
+
+Constructor representing a ket state ``|\psi\rangle``.
+"""
+struct Ket <: QuantumObjectType end
+
+Base.show(io::IO, ::Ket) = print(io, "Ket()")
+
+@doc raw"""
+ Operator <: QuantumObjectType
+
+Constructor representing an operator ``\hat{O}``.
+"""
+struct Operator <: QuantumObjectType end
+
+Base.show(io::IO, ::Operator) = print(io, "Operator()")
+
+@doc raw"""
+ SuperOperator <: SuperOperatorType
+
+Constructor representing a super-operator ``\hat{\mathcal{O}}`` acting on vectorized density operator matrices.
+"""
+struct SuperOperator <: SuperOperatorType end
+
+Base.show(io::IO, ::SuperOperator) = print(io, "SuperOperator()")
+
+@doc raw"""
+ OperatorBra <: QuantumObjectType
+
+Constructor representing a bra state in the [`SuperOperator`](@ref) formalism ``\langle\langle\rho|``.
+"""
+struct OperatorBra <: QuantumObjectType end
+
+Base.show(io::IO, ::OperatorBra) = print(io, "OperatorBra()")
+
+@doc raw"""
+ OperatorKet <: QuantumObjectType
+
+Constructor representing a ket state in the [`SuperOperator`](@ref) formalism ``|\rho\rangle\rangle``.
+"""
+struct OperatorKet <: QuantumObjectType end
+
+Base.show(io::IO, ::OperatorKet) = print(io, "OperatorKet()")
+
+@doc raw"""
+ size(A::AbstractQuantumObject)
+ size(A::AbstractQuantumObject, idx::Int)
+ shape(A::AbstractQuantumObject)
+ shape(A::AbstractQuantumObject, idx::Int)
+
+Returns a tuple containing each dimensions of the array in the [`AbstractQuantumObject`](@ref).
+
+Optionally, you can specify an index (`idx`) to just get the corresponding dimension of the array.
+
+!!! note
+ `shape` is a synonym of `size`.
+"""
+Base.size(A::AbstractQuantumObject) = size(A.data)
+Base.size(A::AbstractQuantumObject, idx::Int) = size(A.data, idx)
+
+Base.copy(A::AbstractQuantumObject) = get_typename_wrapper(A)(copy(A.data), A.type, A.dimensions)
+
+Base.getindex(A::AbstractQuantumObject, inds...) = getindex(A.data, inds...)
+Base.setindex!(A::AbstractQuantumObject, val, inds...) = setindex!(A.data, val, inds...)
+
+@doc raw"""
+ eltype(A::AbstractQuantumObject)
+
+Returns the elements type of the matrix or vector corresponding to the [`AbstractQuantumObject`](@ref) `A`.
+"""
+Base.eltype(A::AbstractQuantumObject) = eltype(A.data)
+
+@doc raw"""
+ length(A::AbstractQuantumObject)
+
+Returns the length of the matrix or vector corresponding to the [`AbstractQuantumObject`](@ref) `A`.
+"""
+Base.length(A::AbstractQuantumObject) = length(A.data)
+
+Base.isequal(A::AbstractQuantumObject, B::AbstractQuantumObject) =
+ isequal(A.type, B.type) && isequal(A.dimensions, B.dimensions) && isequal(A.data, B.data)
+Base.isapprox(A::AbstractQuantumObject, B::AbstractQuantumObject; kwargs...) =
+ isequal(A.type, B.type) && isequal(A.dimensions, B.dimensions) && isapprox(A.data, B.data; kwargs...)
+Base.:(==)(A::AbstractQuantumObject, B::AbstractQuantumObject) =
+ (A.type == B.type) && (A.dimensions == B.dimensions) && (A.data == B.data)
+
+function check_dimensions(dimensions_list::NTuple{N,AbstractDimensions}) where {N}
+ allequal(dimensions_list) ||
+ throw(DimensionMismatch("The quantum objects should have the same Hilbert `dimensions`."))
+ return nothing
+end
+check_dimensions(Qobj_tuple::NTuple{N,AbstractQuantumObject}) where {N} =
+ check_dimensions(getfield.(Qobj_tuple, :dimensions))
+check_dimensions(A::AbstractQuantumObject...) = check_dimensions(A)
+
+_check_QuantumObject(
+ type::ObjType,
+ dimensions::GeneralDimensions,
+ m::Int,
+ n::Int,
+) where {ObjType<:Union{Ket,Bra,SuperOperator,OperatorBra,OperatorKet}} = throw(
+ DomainError(
+ _get_dims_string(dimensions),
+ "The given `dims` is not compatible with type = $type, should be a single list of integers.",
+ ),
+)
+
+function _check_QuantumObject(type::Ket, dimensions::Dimensions, m::Int, n::Int)
+ (n != 1) && throw(DomainError((m, n), "The size of the array is not compatible with Ket"))
+ (prod(dimensions) != m) && throw(
+ DimensionMismatch("Ket with dims = $(_get_dims_string(dimensions)) does not fit the array size = $((m, n))."),
+ )
+ return nothing
+end
+
+function _check_QuantumObject(type::Bra, dimensions::Dimensions, m::Int, n::Int)
+ (m != 1) && throw(DomainError((m, n), "The size of the array is not compatible with Bra"))
+ (prod(dimensions) != n) && throw(
+ DimensionMismatch("Bra with dims = $(_get_dims_string(dimensions)) does not fit the array size = $((m, n))."),
+ )
+ return nothing
+end
+
+function _check_QuantumObject(type::Operator, dimensions::Dimensions, m::Int, n::Int)
+ L = prod(dimensions)
+ (L == m == n) || throw(
+ DimensionMismatch(
+ "Operator with dims = $(_get_dims_string(dimensions)) does not fit the array size = $((m, n)).",
+ ),
+ )
+ return nothing
+end
+
+function _check_QuantumObject(type::Operator, dimensions::GeneralDimensions, m::Int, n::Int)
+ ((m == 1) || (n == 1)) && throw(DomainError((m, n), "The size of the array is not compatible with Operator"))
+ ((prod(dimensions.to) != m) || (prod(dimensions.from) != n)) && throw(
+ DimensionMismatch(
+ "Operator with dims = $(_get_dims_string(dimensions)) does not fit the array size = $((m, n)).",
+ ),
+ )
+ return nothing
+end
+
+function _check_QuantumObject(type::SuperOperator, dimensions::Dimensions, m::Int, n::Int)
+ (m != n) && throw(DomainError((m, n), "The size of the array is not compatible with SuperOperator"))
+ (prod(dimensions) != sqrt(m)) && throw(
+ DimensionMismatch(
+ "SuperOperator with dims = $(_get_dims_string(dimensions)) does not fit the array size = $((m, n)).",
+ ),
+ )
+ return nothing
+end
+
+function _check_QuantumObject(type::OperatorKet, dimensions::Dimensions, m::Int, n::Int)
+ (n != 1) && throw(DomainError((m, n), "The size of the array is not compatible with OperatorKet"))
+ (prod(dimensions) != sqrt(m)) && throw(
+ DimensionMismatch(
+ "OperatorKet with dims = $(_get_dims_string(dimensions)) does not fit the array size = $((m, n)).",
+ ),
+ )
+ return nothing
+end
+
+function _check_QuantumObject(type::OperatorBra, dimensions::Dimensions, m::Int, n::Int)
+ (m != 1) && throw(DomainError((m, n), "The size of the array is not compatible with OperatorBra"))
+ (prod(dimensions) != sqrt(n)) && throw(
+ DimensionMismatch(
+ "OperatorBra with dims = $(_get_dims_string(dimensions)) does not fit the array size = $((m, n)).",
+ ),
+ )
+ return nothing
+end
+
+_check_type(::T) where {T<:Union{Nothing,<:QuantumObjectType}} = T
+_check_type(::Type{T}) where {T} =
+ throw(ArgumentError("The argument `$T` is not valid. You may probably want to use `$T()` instead."))
+_check_type(t) = throw(ArgumentError("The argument $t is not valid. It should be a subtype of `QuantumObjectType`."))
+
+function Base.getproperty(A::AbstractQuantumObject, key::Symbol)
+ # a comment here to avoid bad render by JuliaFormatter
+ if key === :dims
+ return dimensions_to_dims(getfield(A, :dimensions))
+ else
+ return getfield(A, key)
+ end
+end
+
+# this returns `to` in GeneralDimensions representation
+get_dimensions_to(A::AbstractQuantumObject{Ket,<:Dimensions}) = A.dimensions.to
+get_dimensions_to(A::AbstractQuantumObject{Bra,<:Dimensions}) = space_one_list(A.dimensions.to)
+get_dimensions_to(A::AbstractQuantumObject{Operator,<:Dimensions}) = A.dimensions.to
+get_dimensions_to(A::AbstractQuantumObject{Operator,<:GeneralDimensions}) = A.dimensions.to
+get_dimensions_to(
+ A::AbstractQuantumObject{ObjType,<:Dimensions},
+) where {ObjType<:Union{SuperOperator,OperatorBra,OperatorKet}} = A.dimensions.to
+
+# this returns `from` in GeneralDimensions representation
+get_dimensions_from(A::AbstractQuantumObject{Ket,<:Dimensions}) = space_one_list(A.dimensions.to)
+get_dimensions_from(A::AbstractQuantumObject{Bra,<:Dimensions}) = A.dimensions.to
+get_dimensions_from(A::AbstractQuantumObject{Operator,<:Dimensions}) = A.dimensions.to
+get_dimensions_from(A::AbstractQuantumObject{Operator,<:GeneralDimensions}) = A.dimensions.from
+get_dimensions_from(
+ A::AbstractQuantumObject{ObjType,<:Dimensions},
+) where {ObjType<:Union{SuperOperator,OperatorBra,OperatorKet}} = A.dimensions.to
+
+# this creates a list of Space(1), it is used to generate `from` for Ket, and `to` for Bra
+space_one_list(dimensions::NTuple{N,AbstractSpace}) where {N} =
+ ntuple(i -> Space(1), Val(sum(_get_dims_length, dimensions)))
+_get_dims_length(::Space) = 1
+_get_dims_length(::EnrSpace{N}) where {N} = N
+
+# functions for getting Float or Complex element type
+_float_type(A::AbstractQuantumObject) = _float_type(eltype(A))
+_complex_float_type(A::AbstractQuantumObject) = _complex_float_type(eltype(A))
diff --git a/src/qobj/quantum_object_evo.jl b/src/qobj/quantum_object_evo.jl
new file mode 100644
index 000000000..8e4de483b
--- /dev/null
+++ b/src/qobj/quantum_object_evo.jl
@@ -0,0 +1,537 @@
+#=
+This file defines the QuantumObjectEvolution (QobjEvo) structure.
+=#
+
+export QuantumObjectEvolution
+
+@doc raw"""
+ struct QuantumObjectEvolution{ObjType<:QuantumObjectType,DimType<:AbstractDimensions,DataType<:AbstractSciMLOperator} <: AbstractQuantumObject{ObjType,DimType,DataType}
+ data::DataType
+ type::ObjType
+ dimensions::DimType
+ end
+
+Julia struct representing any time-dependent quantum object. The `data` field is a `AbstractSciMLOperator` object that represents the time-dependent quantum object. It can be seen as
+
+```math
+\hat{O}(t) = \sum_{i} c_i(p, t) \hat{O}_i
+```
+
+where ``c_i(p, t)`` is a function that depends on the parameters `p` and time `t`, and ``\hat{O}_i`` are the operators that form the quantum object.
+
+For time-independent cases, see [`QuantumObject`](@ref), and for more information about `AbstractSciMLOperator`, see the [SciML](https://docs.sciml.ai/SciMLOperators/stable/) documentation.
+
+!!! note "`dims` property"
+ For a given `H::QuantumObjectEvolution`, `H.dims` or `getproperty(H, :dims)` returns its `dimensions` in the type of integer-vector.
+
+# Examples
+This operator can be initialized in the same way as the QuTiP `QobjEvo` object. For example
+```jldoctest qobjevo
+julia> a = tensor(destroy(10), qeye(2))
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 18 stored entries:
+⎡⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⎦
+
+julia> coef1(p, t) = exp(-1im * t)
+coef1 (generic function with 1 method)
+
+julia> op = QobjEvo(a, coef1)
+
+Quantum Object Evo.: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=true isconstant=false
+ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20)
+```
+
+If there are more than 2 operators, we need to put each set of operator and coefficient function into a two-element `Tuple`, and put all these `Tuple`s together in a larger `Tuple`:
+
+```jldoctest qobjevo
+julia> σm = tensor(qeye(10), sigmam())
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 10 stored entries:
+⎡⠂⡀⠀⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠂⡀⠀⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠂⡀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠂⡀⠀⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠂⡀⎦
+
+julia> coef2(p, t) = sin(t)
+coef2 (generic function with 1 method)
+
+julia> op1 = QobjEvo(((a, coef1), (σm, coef2)))
+
+Quantum Object Evo.: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=true isconstant=false
+(ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20) + ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20))
+```
+
+We can also concretize the operator at a specific time `t`
+```jldoctest qobjevo
+julia> op1(0.1)
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 28 stored entries:
+⎡⠂⡑⢄⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠂⡑⢄⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠂⡑⢄⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠂⡑⢄⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠂⡑⎦
+```
+
+It also supports parameter-dependent time evolution
+```jldoctest qobjevo
+julia> coef1(p, t) = exp(-1im * p.ω1 * t)
+coef1 (generic function with 1 method)
+
+julia> coef2(p, t) = sin(p.ω2 * t)
+coef2 (generic function with 1 method)
+
+julia> op1 = QobjEvo(((a, coef1), (σm, coef2)))
+
+Quantum Object Evo.: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=true isconstant=false
+(ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20) + ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20))
+
+julia> p = (ω1 = 1.0, ω2 = 0.5)
+(ω1 = 1.0, ω2 = 0.5)
+
+julia> op1(p, 0.1)
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 28 stored entries:
+⎡⠂⡑⢄⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠂⡑⢄⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠂⡑⢄⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠂⡑⢄⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠂⡑⎦
+```
+"""
+struct QuantumObjectEvolution{
+ ObjType<:Union{Operator,SuperOperator},
+ DimType<:AbstractDimensions,
+ DataType<:AbstractSciMLOperator,
+} <: AbstractQuantumObject{ObjType,DimType,DataType}
+ data::DataType
+ type::ObjType
+ dimensions::DimType
+
+ function QuantumObjectEvolution(data::DT, type, dims) where {DT<:AbstractSciMLOperator}
+ ObjType = _check_type(type)
+ (type isa Operator || type isa SuperOperator) ||
+ throw(ArgumentError("The type $type is not supported for QuantumObjectEvolution."))
+
+ dimensions = _gen_dimensions(dims)
+
+ _size = _get_size(data)
+ _check_QuantumObject(type, dimensions, _size[1], _size[2])
+
+ return new{ObjType,typeof(dimensions),DT}(data, type, dimensions)
+ end
+end
+
+function Base.show(io::IO, QO::QuantumObjectEvolution)
+ op_data = QO.data
+ println(
+ io,
+ "\nQuantum Object Evo.: type=",
+ QO.type,
+ " dims=",
+ _get_dims_string(QO.dimensions),
+ " size=",
+ size(op_data),
+ " ishermitian=",
+ ishermitian(op_data),
+ " isconstant=",
+ isconstant(op_data),
+ )
+ return show(io, MIME("text/plain"), op_data)
+end
+
+@doc raw"""
+ QobjEvo(data::AbstractSciMLOperator; type = Operator(), dims = nothing)
+ QuantumObjectEvolution(data::AbstractSciMLOperator; type = Operator(), dims = nothing)
+
+Generate a [`QuantumObjectEvolution`](@ref) object from a [`SciMLOperator`](https://github.com/SciML/SciMLOperators.jl), in the same way as [`QuantumObject`](@ref) for `AbstractArray` inputs.
+
+Note that `QobjEvo` is a synonym of `QuantumObjectEvolution`
+"""
+function QuantumObjectEvolution(data::AbstractSciMLOperator; type = Operator(), dims = nothing)
+ _size = _get_size(data)
+ _check_type(type)
+
+ if dims isa Nothing
+ if type isa Operator
+ dims =
+ (_size[1] == _size[2]) ? Dimensions(_size[1]) :
+ GeneralDimensions(SVector{2}(SVector{1}(_size[1]), SVector{1}(_size[2])))
+ elseif type isa SuperOperator
+ dims = Dimensions(isqrt(_size[2]))
+ end
+ end
+
+ return QuantumObjectEvolution(data, type, dims)
+end
+
+@doc raw"""
+ QobjEvo(op_func_list::Union{Tuple,AbstractQuantumObject}; type=nothing)
+ QuantumObjectEvolution(op_func_list::Union{Tuple,AbstractQuantumObject}; type=nothing)
+
+Generate [`QuantumObjectEvolution`](@ref).
+
+# Arguments
+- `op_func_list::Union{Tuple,AbstractQuantumObject}`: A tuple of tuples or operators.
+
+!!! warning "Beware of type-stability!"
+ Please note that, unlike QuTiP, this function doesn't support `op_func_list` as `Vector` type. This is related to the type-stability issue. See the Section [The Importance of Type-Stability](@ref doc:Type-Stability) for more details.
+
+The `type` parameter is used to specify the type of the [`QuantumObject`](@ref), either `Operator` or `SuperOperator`.
+
+!!! note
+ `QobjEvo` is a synonym of `QuantumObjectEvolution`.
+
+# Examples
+This operator can be initialized in the same way as the QuTiP `QobjEvo` object. For example
+```jldoctest qobjevo
+julia> a = tensor(destroy(10), qeye(2))
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 18 stored entries:
+⎡⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⎦
+
+julia> σm = tensor(qeye(10), sigmam())
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 10 stored entries:
+⎡⠂⡀⠀⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠂⡀⠀⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠂⡀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠂⡀⠀⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠂⡀⎦
+
+julia> coef1(p, t) = exp(-1im * t)
+coef1 (generic function with 1 method)
+
+julia> coef2(p, t) = sin(t)
+coef2 (generic function with 1 method)
+
+julia> op1 = QobjEvo(((a, coef1), (σm, coef2)))
+
+Quantum Object Evo.: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=true isconstant=false
+(ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20) + ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20))
+```
+
+We can also concretize the operator at a specific time `t`
+```jldoctest qobjevo
+julia> op1(0.1)
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 28 stored entries:
+⎡⠂⡑⢄⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠂⡑⢄⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠂⡑⢄⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠂⡑⢄⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠂⡑⎦
+```
+
+It also supports parameter-dependent time evolution
+```jldoctest qobjevo
+julia> coef1(p, t) = exp(-1im * p.ω1 * t)
+coef1 (generic function with 1 method)
+
+julia> coef2(p, t) = sin(p.ω2 * t)
+coef2 (generic function with 1 method)
+
+julia> op1 = QobjEvo(((a, coef1), (σm, coef2)))
+
+Quantum Object Evo.: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=true isconstant=false
+(ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20) + ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20))
+
+julia> p = (ω1 = 1.0, ω2 = 0.5)
+(ω1 = 1.0, ω2 = 0.5)
+
+julia> op1(p, 0.1)
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 28 stored entries:
+⎡⠂⡑⢄⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠂⡑⢄⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠂⡑⢄⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠂⡑⢄⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠂⡑⎦
+```
+"""
+function QuantumObjectEvolution(op_func_list::Tuple; type = nothing)
+ op, data = _QobjEvo_generate_data(op_func_list)
+ dims = op.dimensions
+ _check_type(type)
+
+ if type isa Nothing
+ type = op.type
+ end
+
+ # Preallocate the SciMLOperator cache using a dense vector as a reference
+ v0 = to_dense(similar(op.data, size(op, 1)))
+ data = cache_operator(data, v0)
+
+ return QuantumObjectEvolution(data, type, dims)
+end
+
+# this is a extra method if user accidentally specify `QuantumObjectEvolution( (op, func) )` or `QuantumObjectEvolution( ((op, func)) )`
+QuantumObjectEvolution(op_func::Tuple{<:QuantumObject,<:Function}; type = nothing) =
+ QuantumObjectEvolution((op_func,); type = type)
+
+@doc raw"""
+ QuantumObjectEvolution(op::QuantumObject, f::Function; type = nothing)
+ QobjEvo(op::QuantumObject, f::Function; type = nothing)
+
+Generate [`QuantumObjectEvolution`](@ref).
+
+# Notes
+- The `f` parameter is time-dependent coefficient that multiplies the operator. The `type` parameter is used to specify the type of the [`QuantumObject`](@ref), either `Operator` or `SuperOperator`.
+- `QobjEvo` is a synonym of `QuantumObjectEvolution`.
+
+# Examples
+```jldoctest
+julia> a = tensor(destroy(10), qeye(2))
+
+Quantum Object: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=false
+20×20 SparseMatrixCSC{ComplexF64, Int64} with 18 stored entries:
+⎡⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⎤
+⎢⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⎥
+⎢⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⎥
+⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⎦
+
+julia> coef(p, t) = exp(-1im * t)
+coef (generic function with 1 method)
+
+julia> op = QobjEvo(a, coef)
+
+Quantum Object Evo.: type=Operator() dims=[10, 2] size=(20, 20) ishermitian=true isconstant=false
+ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20)
+```
+"""
+QuantumObjectEvolution(op::QuantumObject, f::Function; type = nothing) = QuantumObjectEvolution(((op, f),); type = type)
+
+function QuantumObjectEvolution(op::QuantumObject; type = nothing)
+ _check_type(type)
+ if type isa Nothing
+ type = op.type
+ end
+ return QuantumObjectEvolution(_make_SciMLOperator(op), type, op.dimensions)
+end
+
+function QuantumObjectEvolution(op::QuantumObjectEvolution; type = nothing)
+ _check_type(type)
+ if type isa Nothing
+ type = op.type
+ elseif type != op.type
+ throw(
+ ArgumentError(
+ "The type of the QuantumObjectEvolution object cannot be changed when using another QuantumObjectEvolution object as input.",
+ ),
+ )
+ end
+ return QuantumObjectEvolution(op.data, type, op.dimensions)
+end
+
+#=
+ _QobjEvo_generate_data(op_func_list::Tuple)
+
+Parse the `op_func_list` and generate the data for the `QuantumObjectEvolution` object. The `op_func_list` is a tuple of tuples or operators. Each element of the tuple can be a tuple with two elements (operator, function) or an operator. The function is used to generate the time-dependent coefficients for the operators. During the parsing, the dimensions of the operators are checked to be the same, and all the constant operators are summed together.
+
+# Arguments
+- `op_func_list::Tuple`: A tuple of tuples or operators.
+=#
+function _QobjEvo_generate_data(op_func_list::Tuple)
+ first_op = _QobjEvo_get_first_op(op_func_list[1])
+
+ ops_constant = filter(op_func_list) do x
+ if x isa QuantumObject
+ x.type == first_op.type || throw(ArgumentError("The types of the operators must be the same."))
+ x.dimensions == first_op.dimensions ||
+ throw(ArgumentError("The dimensions of the operators must be the same."))
+ return true
+ elseif x isa Tuple
+ return false
+ else
+ throw(ArgumentError("Each element of the tuple must be either a QuantumObject or a Tuple."))
+ end
+ end
+
+ ops_time_dep = filter(op_func_list) do x
+ if x isa Tuple
+ _QobjEvo_check_op_func(x)
+ op = x[1]
+ _QobjEvo_check_op(op)
+ op.type == first_op.type || throw(ArgumentError("The types of the operators must be the same."))
+ op.dimensions == first_op.dimensions ||
+ throw(ArgumentError("The dimensions of the operators must be the same."))
+ return true
+ elseif x isa QuantumObject
+ return false
+ else
+ throw(ArgumentError("Each element of the tuple must be either a QuantumObject or a Tuple."))
+ end
+ end
+
+ data_const = isempty(ops_constant) ? zero(eltype(first_op)) : _make_SciMLOperator(sum(ops_constant))
+ data_td =
+ length(ops_time_dep) == 1 ? _make_SciMLOperator(ops_time_dep[1]) :
+ AddedOperator(map(_make_SciMLOperator, ops_time_dep))
+ data = data_const + data_td
+
+ return first_op, data
+end
+
+function _QobjEvo_check_op(op::AbstractQuantumObject)
+ (isoper(op) || issuper(op)) || throw(ArgumentError("The element must be a Operator or SuperOperator."))
+ return nothing
+end
+
+function _QobjEvo_check_op_func(op_func::Tuple)
+ length(op_func) == 2 || throw(ArgumentError("The tuple must have two elements."))
+ _QobjEvo_check_op(op_func[1])
+ (op_func[2] isa Function) || throw(ArgumentError("The second element must be a function."))
+ methods(op_func[2], (Any, Real)) |> length == 0 && throw(
+ ArgumentError(
+ "The following function must only accept two arguments: `$(nameof(op_func[2]))(p, t)` with t<:Real",
+ ),
+ )
+ return nothing
+end
+
+_QobjEvo_get_first_op(op_func_list_1::Union{Tuple,AbstractQuantumObject}) =
+ if op_func_list_1 isa Tuple
+ _QobjEvo_check_op_func(op_func_list_1)
+ op = op_func_list_1[1]
+ _QobjEvo_check_op(op)
+ return op
+ else
+ op = op_func_list_1
+ _QobjEvo_check_op(op)
+ return op
+ end
+
+function _make_SciMLOperator(op_func::Tuple)
+ op, coef = op_func
+ T = eltype(op)
+ update_func = (a, u, p, t) -> coef(p, t)
+ return ScalarOperator(zero(T), update_func) * _promote_to_scimloperator(op.data)
+end
+_make_SciMLOperator(op::AbstractQuantumObject) = _promote_to_scimloperator(op.data)
+
+_promote_to_scimloperator(data::AbstractMatrix) = MatrixOperator(data)
+_promote_to_scimloperator(data::AbstractSciMLOperator) = data
+
+@doc raw"""
+ (A::QuantumObjectEvolution)(ψout, ψin, p, t)
+
+Apply the time-dependent [`QuantumObjectEvolution`](@ref) object `A` to the input state `ψin` at time `t` with parameters `p`. The output state is stored in `ψout`. This function mimics the behavior of a `AbstractSciMLOperator` object.
+
+# Arguments
+- `ψout::QuantumObject`: The output state. It must have the same type as `ψin`.
+- `ψin::QuantumObject`: The input state. It must be either a [`Ket`](@ref) or a [`OperatorKet`](@ref).
+- `p`: The parameters of the time-dependent coefficients.
+- `t`: The time at which the coefficients are evaluated.
+
+# Returns
+- `ψout::QuantumObject`: The output state.
+
+# Examples
+```jldoctest
+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> coef1(p, t) = sin(t)
+coef1 (generic function with 1 method)
+
+julia> coef2(p, t) = cos(t)
+coef2 (generic function with 1 method)
+
+julia> A = QobjEvo(((a, coef1), (a', coef2)))
+
+Quantum Object Evo.: type=Operator() dims=[20] size=(20, 20) ishermitian=true isconstant=false
+(ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20) + ScalarOperator(0.0 + 0.0im) * MatrixOperator(20 × 20))
+
+julia> ψ1 = fock(20, 3);
+
+julia> ψ2 = zero_ket(20);
+
+julia> A(ψ2, ψ1, nothing, 0.1) ≈ A(0.1) * ψ1
+true
+```
+"""
+function (A::QuantumObjectEvolution)(
+ ψout::QuantumObject{QobjType},
+ ψin::QuantumObject{QobjType},
+ p,
+ t,
+) where {QobjType<:Union{Ket,OperatorKet}}
+ check_dimensions(A, ψout, ψin)
+
+ if isoper(A) && isoperket(ψin)
+ throw(ArgumentError("The input state must be a Ket if the QuantumObjectEvolution object is an Operator."))
+ elseif issuper(A) && isket(ψin)
+ throw(
+ ArgumentError(
+ "The input state must be an OperatorKet if the QuantumObjectEvolution object is a SuperOperator.",
+ ),
+ )
+ end
+
+ A.data(ψout.data, ψin.data, nothing, p, t)
+
+ return ψout
+end
+
+@doc raw"""
+ (A::QuantumObjectEvolution)(ψ, p, t)
+
+Apply the time-dependent [`QuantumObjectEvolution`](@ref) object `A` to the input state `ψ` at time `t` with parameters `p`. Out-of-place version of [`(A::QuantumObjectEvolution)(ψout, ψin, p, t)`](@ref). The output state is stored in a new [`QuantumObject`](@ref) object. This function mimics the behavior of a `AbstractSciMLOperator` object.
+"""
+function (A::QuantumObjectEvolution)(ψ::QuantumObject{QobjType}, p, t) where {QobjType<:Union{Ket,OperatorKet}}
+ ψout = QuantumObject(similar(ψ.data), ψ.type, ψ.dimensions)
+ return A(ψout, ψ, p, t)
+end
+
+@doc raw"""
+ (A::QuantumObjectEvolution)(p, t)
+
+Calculate the time-dependent [`QuantumObjectEvolution`](@ref) object `A` at time `t` with parameters `p`.
+
+# Arguments
+- `p`: The parameters of the time-dependent coefficients.
+- `t`: The time at which the coefficients are evaluated.
+
+# Returns
+- `A::QuantumObject`: The output state.
+"""
+function (A::QuantumObjectEvolution)(p, t)
+ # We put 0 in the place of `u` because the time-dependence doesn't depend on the state
+ update_coefficients!(A.data, 0, p, t)
+ return QuantumObject(concretize(A.data), A.type, A.dimensions)
+end
+
+(A::QuantumObjectEvolution)(t) = A(nothing, t)
+
+#=
+`promote_type` should be applied on types. Here I define `promote_op_type` because it is applied to operators.
+=#
+promote_op_type(A::QuantumObjectEvolution, B::QuantumObjectEvolution) = get_typename_wrapper(A)
+promote_op_type(A::QuantumObjectEvolution, B::QuantumObject) = get_typename_wrapper(A)
+promote_op_type(A::QuantumObject, B::QuantumObjectEvolution) = get_typename_wrapper(B)
+promote_op_type(A::QuantumObject, B::QuantumObject) = get_typename_wrapper(A)
diff --git a/src/qobj/space.jl b/src/qobj/space.jl
new file mode 100644
index 000000000..90b199759
--- /dev/null
+++ b/src/qobj/space.jl
@@ -0,0 +1,25 @@
+#=
+This file defines the Hilbert space structure.
+=#
+
+export AbstractSpace, Space
+
+abstract type AbstractSpace end
+
+@doc raw"""
+ struct Space <: AbstractSpace
+ size::Int
+ end
+
+A structure that describes a single Hilbert space with size = `size`.
+"""
+struct Space <: AbstractSpace
+ size::Int
+
+ function Space(size::Int)
+ (size < 1) && throw(DomainError(size, "The size of Space must be positive integer (≥ 1)."))
+ return new(size)
+ end
+end
+
+dimensions_to_dims(s::Space) = SVector{1,Int}(s.size)
diff --git a/src/qobj/states.jl b/src/qobj/states.jl
index 04a3cdccd..b9bfe7ba6 100644
--- a/src/qobj/states.jl
+++ b/src/qobj/states.jl
@@ -14,14 +14,14 @@ 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::Union{AbstractVector{Int}, Tuple}`: list of dimensions representing the each number of basis in the subsystems.
+- `dimensions::Union{Dimensions,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.
+ It is highly recommended to use `zero_ket(dimensions)` with `dimensions` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) 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::Union{AbstractVector{Int},Tuple}) =
- QuantumObject(zeros(ComplexF64, prod(dimensions)), Ket, dimensions)
+zero_ket(dimensions::Int) = QuantumObject(zeros(ComplexF64, dimensions), Ket(), dimensions)
+zero_ket(dimensions::Union{Dimensions,AbstractVector{Int},Tuple}) =
+ QuantumObject(zeros(ComplexF64, prod(dimensions)), Ket(), dimensions)
@doc raw"""
fock(N::Int, j::Int=0; dims::Union{Int,AbstractVector{Int},Tuple}=N, sparse::Union{Bool,Val}=Val(false))
@@ -31,16 +31,15 @@ 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.
+ 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` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) 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, j::Int = 0; dims::Union{Int,AbstractVector{Int},Tuple} = N, sparse::Union{Bool,Val} = Val(false))
- if getVal(makeVal(sparse))
+ if getVal(sparse)
array = sparsevec([j + 1], [1.0 + 0im], N)
else
- array = zeros(ComplexF64, N)
- array[j+1] = 1
+ array = [i == (j + 1) ? 1.0 + 0im : 0.0 + 0im for i in 1:N]
end
- return QuantumObject(array; type = Ket, dims = dims)
+ return QuantumObject(array; type = Ket(), dims = dims)
end
@doc raw"""
@@ -51,7 +50,7 @@ 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.
+ If you want to keep type stability, it is recommended to use `basis(N, j, dims=dims)` with `dims` as a `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) 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, j::Int = 0; dims::Union{Int,AbstractVector{Int},Tuple} = N) = fock(N, j, dims = dims)
@@ -71,16 +70,16 @@ 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::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems.
+- `dimensions::Union{Dimensions,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.
+ If you want to keep type stability, it is recommended to use `rand_ket(dimensions)` with `dimensions` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) to keep type stability. See the [related Section](@ref doc:Type-Stability) about type stability for more details.
"""
rand_ket(dimensions::Int) = rand_ket(SVector(dimensions))
-function rand_ket(dimensions::Union{AbstractVector{Int},Tuple})
+function rand_ket(dimensions::Union{Dimensions,AbstractVector{Int},Tuple})
N = prod(dimensions)
ψ = rand(ComplexF64, N) .- (0.5 + 0.5im)
- return QuantumObject(normalize!(ψ); type = Ket, dims = dimensions)
+ return QuantumObject(normalize!(ψ); type = Ket(), dims = dimensions)
end
@doc raw"""
@@ -91,7 +90,7 @@ 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.
+ 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` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) 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,
@@ -128,12 +127,12 @@ Density matrix for a thermal state (generating thermal state probabilities) with
"""
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)
+ N_list = Array{Float64}(0:(N-1))
data = exp.(-β .* N_list)
- if getVal(makeVal(sparse))
- return QuantumObject(spdiagm(0 => data ./ sum(data)), Operator, N)
+ if getVal(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
@@ -144,15 +143,16 @@ 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::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems.
+- `dimensions::Union{Dimensions,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.
+ If you want to keep type stability, it is recommended to use `maximally_mixed_dm(dimensions)` with `dimensions` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) 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, SVector(dimensions))
-function maximally_mixed_dm(dimensions::Union{AbstractVector{Int},Tuple})
+maximally_mixed_dm(dimensions::Int) =
+ QuantumObject(I(dimensions) / complex(dimensions), Operator(), SVector(dimensions))
+function maximally_mixed_dm(dimensions::Union{Dimensions,AbstractVector{Int},Tuple})
N = prod(dimensions)
- return QuantumObject(I(N) / complex(N), Operator, dimensions)
+ return QuantumObject(I(N) / complex(N), Operator(), dimensions)
end
@doc raw"""
@@ -162,19 +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::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems.
+- `dimensions::Union{Dimensions,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.
+ If you want to keep type stability, it is recommended to use `rand_dm(dimensions; rank=rank)` with `dimensions` as `Tuple` or `SVector` from [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) 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(SVector(dimensions), rank = rank)
-function rand_dm(dimensions::Union{AbstractVector{Int},Tuple}; rank::Int = prod(dimensions))
+function rand_dm(dimensions::Union{Dimensions,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."))
@@ -182,7 +182,7 @@ function rand_dm(dimensions::Union{AbstractVector{Int},Tuple}; rank::Int = prod(
X = _Ginibre_ensemble(N, rank)
ρ = X * X'
ρ /= tr(ρ)
- return QuantumObject(ρ; type = Operator, dims = dimensions)
+ return QuantumObject(ρ; type = Operator(), dims = dimensions)
end
@doc raw"""
@@ -190,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).
"""
@@ -213,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
@@ -250,9 +252,10 @@ Here, `x = 1` (`z = 1`) means applying Pauli-``X`` ( Pauli-``Z``) unitary transf
# Example
-```
+```jldoctest
julia> bell_state(0, 0)
-Quantum Object: type=Ket dims=[2, 2] size=(4,)
+
+Quantum Object: type=Ket() dims=[2, 2] size=(4,)
4-element Vector{ComplexF64}:
0.7071067811865475 + 0.0im
0.0 + 0.0im
@@ -260,7 +263,8 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,)
0.7071067811865475 + 0.0im
julia> bell_state(Val(1), Val(0))
-Quantum Object: type=Ket dims=[2, 2] size=(4,)
+
+Quantum Object: type=Ket() dims=[2, 2] size=(4,)
4-element Vector{ComplexF64}:
0.0 + 0.0im
0.7071067811865475 + 0.0im
@@ -272,10 +276,10 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,)
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"""
@@ -283,7 +287,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()
@@ -296,9 +300,9 @@ 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
@@ -317,7 +321,7 @@ Returns the `n`-qubit [W-state](https://en.wikipedia.org/wiki/W_state):
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, ntuple(x -> 2, Val(n)))
+ return QuantumObject(SparseVector(2^n, nzind, nzval), Ket(), ntuple(x -> 2, Val(n)))
end
w_state(n::Int) = w_state(Val(n))
@@ -338,6 +342,6 @@ Here, `d` specifies the dimension of each qudit. Default to `d=2` (qubit).
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, ntuple(x -> d, Val(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 61945013b..0a561c6e1 100644
--- a/src/qobj/superoperators.jl
+++ b/src/qobj/superoperators.jl
@@ -2,10 +2,10 @@
Functions for generating (common) quantum super-operators.
=#
-export spre, spost, sprepost, lindblad_dissipator
+export spre, spost, sprepost, liouvillian, lindblad_dissipator
# intrinsic functions for super-operators
-# (keep these because they take AbstractMatrix as input)
+## keep these because they take AbstractMatrix as input and ensure the output is sparse matrix
_spre(A::AbstractMatrix, Id::AbstractMatrix) = kron(Id, sparse(A))
_spre(A::AbstractSparseMatrix, Id::AbstractMatrix) = kron(Id, A)
_spost(B::AbstractMatrix, Id::AbstractMatrix) = kron(transpose(sparse(B)), Id)
@@ -14,9 +14,55 @@ _sprepost(A::AbstractMatrix, B::AbstractMatrix) = kron(transpose(sparse(B)), spa
_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)
+function _sprepost(A, B) # for any other input types
+ Id_cache = I(size(A, 1))
+ return _spre(A, Id_cache) * _spost(B, Id_cache)
+end
+
+## if input is AbstractSciMLOperator
+## some of them are optimized to speed things up
+## the rest of the SciMLOperators will just use lazy tensor (and prompt a warning)
+_spre(A::MatrixOperator, Id::AbstractMatrix) = MatrixOperator(_spre(A.A, Id))
+_spre(A::ScaledOperator, Id::AbstractMatrix) = ScaledOperator(A.λ, _spre(A.L, Id))
+_spre(A::AddedOperator, Id::AbstractMatrix) = AddedOperator(map(op -> _spre(op, Id), A.ops))
+function _spre(A::AbstractSciMLOperator, Id::AbstractMatrix)
+ _lazy_tensor_warning(Id, A)
+ return kron(Id, A)
+end
+
+_spost(B::MatrixOperator, Id::AbstractMatrix) = MatrixOperator(_spost(B.A, Id))
+_spost(B::ScaledOperator, Id::AbstractMatrix) = ScaledOperator(B.λ, _spost(B.L, Id))
+_spost(B::AddedOperator, Id::AbstractMatrix) = AddedOperator(map(op -> _spost(op, Id), B.ops))
+function _spost(B::AbstractSciMLOperator, Id::AbstractMatrix)
+ B_T = transpose(B)
+ _lazy_tensor_warning(B_T, Id)
+ return kron(B_T, Id)
+end
+
+## intrinsic liouvillian
+_liouvillian(H::MT, Id::AbstractMatrix) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}} =
+ -1im * (_spre(H, Id) - _spost(H, Id))
+_liouvillian(H::MatrixOperator, Id::AbstractMatrix) = MatrixOperator(_liouvillian(H.A, Id))
+_liouvillian(H::ScaledOperator, Id::AbstractMatrix) = ScaledOperator(H.λ, _liouvillian(H.L, Id))
+_liouvillian(H::AddedOperator, Id::AbstractMatrix) = AddedOperator(map(op -> _liouvillian(op, Id), H.ops))
+
+# intrinsic lindblad_dissipator
+function _lindblad_dissipator(O::MT, Id::AbstractMatrix) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}}
+ Od_O = O' * O
+ return _sprepost(O, O') - (_spre(Od_O, Id) + _spost(Od_O, Id)) / 2
+end
+function _lindblad_dissipator(O::MatrixOperator, Id::AbstractMatrix)
+ _O = O.A
+ Od_O = _O' * _O
+ return MatrixOperator(_sprepost(_O, _O') - (_spre(Od_O, Id) + _spost(Od_O, Id)) / 2)
+end
+function _lindblad_dissipator(O::ScaledOperator, Id::AbstractMatrix)
+ λc_λ = conj(O.λ) * O.λ
+ return ScaledOperator(λc_λ, _lindblad_dissipator(O.L, Id))
+end
@doc raw"""
- spre(A::QuantumObject, Id_cache=I(size(A,1)))
+ spre(A::AbstractQuantumObject, Id_cache=I(size(A,1)))
Returns the [`SuperOperator`](@ref) form of `A` acting on the left of the density matrix operator: ``\mathcal{O} \left(\hat{A}\right) \left[ \hat{\rho} \right] = \hat{A} \hat{\rho}``.
@@ -25,15 +71,17 @@ 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.
-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.
+See also [`spost`](@ref) and [`sprepost`](@ref).
"""
-spre(A::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}, Id_cache = I(size(A, 1))) where {T} =
- QuantumObject(_spre(A.data, Id_cache), SuperOperator, A.dims)
+spre(A::AbstractQuantumObject{Operator}, Id_cache = I(size(A, 1))) =
+ get_typename_wrapper(A)(_spre(A.data, Id_cache), SuperOperator(), A.dimensions)
@doc raw"""
- spost(B::QuantumObject, Id_cache=I(size(B,1)))
+ spost(B::AbstractQuantumObject, Id_cache=I(size(B,1)))
Returns the [`SuperOperator`](@ref) form of `B` acting on the right of the density matrix operator: ``\mathcal{O} \left(\hat{B}\right) \left[ \hat{\rho} \right] = \hat{\rho} \hat{B}``.
@@ -42,37 +90,36 @@ 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.
-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.
+See also [`spre`](@ref) and [`sprepost`](@ref).
"""
-spost(B::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}, Id_cache = I(size(B, 1))) where {T} =
- QuantumObject(_spost(B.data, Id_cache), SuperOperator, B.dims)
+spost(B::AbstractQuantumObject{Operator}, Id_cache = I(size(B, 1))) =
+ get_typename_wrapper(B)(_spost(B.data, Id_cache), SuperOperator(), B.dimensions)
@doc raw"""
- sprepost(A::QuantumObject, B::QuantumObject)
+ sprepost(A::AbstractQuantumObject, B::AbstractQuantumObject)
Returns the [`SuperOperator`](@ref) form of `A` and `B` acting on the left and right of the density matrix operator, respectively: ``\mathcal{O} \left( \hat{A}, \hat{B} \right) \left[ \hat{\rho} \right] = \hat{A} \hat{\rho} \hat{B}``.
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).
"""
-function sprepost(
- A::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- B::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
-) where {T1,T2}
- A.dims != B.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
-
- return QuantumObject(_sprepost(A.data, B.data), SuperOperator, A.dims)
+function sprepost(A::AbstractQuantumObject{Operator}, B::AbstractQuantumObject{Operator})
+ check_dimensions(A, B)
+ return promote_op_type(A, B)(_sprepost(A.data, B.data), SuperOperator(), A.dimensions)
end
@doc raw"""
- lindblad_dissipator(O::QuantumObject, Id_cache=I(size(O,1))
+ lindblad_dissipator(O::AbstractQuantumObject, Id_cache=I(size(O,1))
Returns the Lindblad [`SuperOperator`](@ref) defined as
@@ -81,18 +128,70 @@ Returns the Lindblad [`SuperOperator`](@ref) defined as
\hat{O}^\dagger \hat{O} \hat{\rho} - \hat{\rho} \hat{O}^\dagger \hat{O} \right)
```
-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.
+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.
-See also [`spre`](@ref) and [`spost`](@ref).
+See also [`spre`](@ref), [`spost`](@ref), and [`sprepost`](@ref).
"""
-function lindblad_dissipator(
- O::QuantumObject{<:AbstractArray{T},OperatorQuantumObject},
- Id_cache = I(size(O, 1)),
-) where {T}
- Od_O = O' * O
- return sprepost(O, O') - spre(Od_O, Id_cache) / 2 - spost(Od_O, Id_cache) / 2
-end
+lindblad_dissipator(O::AbstractQuantumObject{Operator}, Id_cache = I(size(O, 1))) =
+ get_typename_wrapper(O)(_lindblad_dissipator(O.data, Id_cache), SuperOperator(), O.dimensions)
# It is already a SuperOperator
-lindblad_dissipator(O::QuantumObject{<:AbstractArray{T},SuperOperatorQuantumObject}, Id_cache) where {T} = O
+lindblad_dissipator(O::AbstractQuantumObject{SuperOperator}, Id_cache = nothing) = O
+
+@doc raw"""
+ liouvillian(H::AbstractQuantumObject, c_ops::Union{Nothing,AbstractVector,Tuple}=nothing, Id_cache=I(prod(H.dimensions)))
+
+Construct the Liouvillian [`SuperOperator`](@ref) for a system Hamiltonian ``\hat{H}`` and a set of collapse operators ``\{\hat{C}_n\}_n``:
+
+```math
+\mathcal{L} [\cdot] = -i[\hat{H}, \cdot] + \sum_n \mathcal{D}(\hat{C}_n) [\cdot]
+```
+
+where
+
+```math
+\mathcal{D}(\hat{C}_n) [\cdot] = \hat{C}_n [\cdot] \hat{C}_n^\dagger - \frac{1}{2} \hat{C}_n^\dagger \hat{C}_n [\cdot] - \frac{1}{2} [\cdot] \hat{C}_n^\dagger \hat{C}_n
+```
+
+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.
+
+See also [`spre`](@ref), [`spost`](@ref), and [`lindblad_dissipator`](@ref).
+"""
+function liouvillian(
+ H::AbstractQuantumObject{OpType},
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ Id_cache::Diagonal = I(prod(H.dimensions)),
+) where {OpType<:Union{Operator,SuperOperator}}
+ L = liouvillian(H, Id_cache)
+ if !(c_ops isa Nothing)
+ L += _sum_lindblad_dissipators(c_ops, Id_cache)
+ end
+ return L
+end
+
+liouvillian(H::Nothing, c_ops::Union{AbstractVector,Tuple}, Id_cache::Diagonal = I(prod(c_ops[1].dims))) =
+ _sum_lindblad_dissipators(c_ops, Id_cache)
+
+liouvillian(H::Nothing, c_ops::Nothing) = 0
+
+liouvillian(H::AbstractQuantumObject{Operator}, Id_cache::Diagonal = I(prod(H.dimensions))) =
+ get_typename_wrapper(H)(_liouvillian(H.data, Id_cache), SuperOperator(), H.dimensions)
+
+liouvillian(H::AbstractQuantumObject{SuperOperator}, Id_cache::Diagonal) = H
+
+function _sum_lindblad_dissipators(c_ops, Id_cache::Diagonal)
+ D = 0
+ # sum all the (time-independent) c_ops first
+ c_ops_ti = filter(op -> isa(op, QuantumObject), c_ops)
+ if !isempty(c_ops_ti)
+ D += mapreduce(op -> lindblad_dissipator(op, Id_cache), +, c_ops_ti)
+ end
+
+ # sum rest of the QobjEvo together
+ c_ops_td = filter(op -> isa(op, QuantumObjectEvolution), c_ops)
+ if !isempty(c_ops_td)
+ D += mapreduce(op -> lindblad_dissipator(op, Id_cache), +, c_ops_td)
+ end
+
+ return D
+end
diff --git a/src/qobj/synonyms.jl b/src/qobj/synonyms.jl
index 5b991d5cb..224c73b59 100644
--- a/src/qobj/synonyms.jl
+++ b/src/qobj/synonyms.jl
@@ -2,63 +2,41 @@
Synonyms of the functions for QuantumObject
=#
-export Qobj, shape, isherm
+export Qobj, QobjEvo, shape, isherm
export trans, dag, matrix_element, unit
-export sqrtm, logm, expm, sinm, cosm
export tensor, ⊗
-export qeye
-
-@doc raw"""
- Qobj(A::AbstractArray; type::QuantumObjectType, dims::Vector{Int})
-
-Generate [`QuantumObject`](@ref)
-
-Note that this functions is same as `QuantumObject(A; type=type, dims=dims)`
-"""
-Qobj(A; kwargs...) = QuantumObject(A; kwargs...)
+export qeye, qeye_like, qzero_like
+export vector_to_operator, operator_to_vector
+export sqrtm, logm, expm, sinm, cosm
@doc raw"""
- shape(A::QuantumObject)
-
-Returns a tuple containing each dimensions of the array in the [`QuantumObject`](@ref).
+ Qobj(A; kwargs...)
-Note that this function is same as `size(A)`
+!!! note
+ `Qobj` is a synonym for generating [`QuantumObject`](@ref). See the docstring of [`QuantumObject`](@ref) for more details.
"""
-shape(A::QuantumObject{<:AbstractArray{T}}) where {T} = size(A.data)
+const Qobj = QuantumObject # we need the docstring here, otherwise the docstring won't be found because QuantumObject is not a public symbol
@doc raw"""
- isherm(A::QuantumObject)
-
-Test whether the [`QuantumObject`](@ref) is Hermitian.
+ QobjEvo(args...; kwargs...)
-Note that this functions is same as `ishermitian(A)`
+!!! note
+ `QobjEvo` is a synonym for generating [`QuantumObjectEvolution`](@ref). See the docstrings of [`QuantumObjectEvolution`](@ref) for more details.
"""
-isherm(A::QuantumObject{<:AbstractArray{T}}) where {T} = ishermitian(A)
-
-@doc raw"""
- trans(A::QuantumObject)
+const QobjEvo = QuantumObjectEvolution # we need the docstring here, otherwise the docstring won't be found because QuantumObjectEvolution is not a public symbol
-Lazy matrix transpose of the [`QuantumObject`](@ref).
+const shape = size
-Note that this function is same as `transpose(A)`
-"""
-trans(
- A::QuantumObject{<:AbstractArray{T},OpType},
-) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = transpose(A)
-
-@doc raw"""
- dag(A::QuantumObject)
+const isherm = ishermitian
-Lazy adjoint (conjugate transposition) of the [`QuantumObject`](@ref)
+const trans = transpose
-Note that this function is same as `adjoint(A)`
-"""
-dag(A::QuantumObject{<:AbstractArray{T}}) where {T} = adjoint(A)
+const dag = adjoint
@doc raw"""
- matrix_element(i::QuantumObject, A::QuantumObject j::QuantumObject)
+ 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)`
@@ -66,66 +44,44 @@ Supports the following inputs:
- `A` is in the type of [`Operator`](@ref), with `i` and `j` are both [`Ket`](@ref).
- `A` is in the type of [`SuperOperator`](@ref), with `i` and `j` are both [`OperatorKet`](@ref)
"""
-matrix_element(
- i::QuantumObject{<:AbstractArray{T1},KetQuantumObject},
- A::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject},
- j::QuantumObject{<:AbstractArray{T3},KetQuantumObject},
-) where {T1<:Number,T2<:Number,T3<:Number} = dot(i, A, j)
-matrix_element(
- i::QuantumObject{<:AbstractArray{T1},OperatorKetQuantumObject},
- A::QuantumObject{<:AbstractArray{T2},SuperOperatorQuantumObject},
- j::QuantumObject{<:AbstractArray{T3},OperatorKetQuantumObject},
-) where {T1<:Number,T2<:Number,T3<:Number} = dot(i, A, j)
+matrix_element(i, A, j) = dot(i, A, j)
-@doc raw"""
- unit(A::QuantumObject, p::Real)
+const unit = normalize
-Return normalized [`QuantumObject`](@ref) so that its `p`-norm equals to unity, i.e. `norm(A, p) == 1`.
+const tensor = kron
+const ⊗ = kron
-Support for the following types of [`QuantumObject`](@ref):
-- If `A` is [`Ket`](@ref) or [`Bra`](@ref), default `p = 2`
-- If `A` is [`Operator`](@ref), default `p = 1`
+const qeye = eye
-Note that this function is same as `normalize(A, p)`
-
-Also, see [`norm`](@ref) about its definition for different types of [`QuantumObject`](@ref).
-"""
-unit(
- A::QuantumObject{<:AbstractArray{T},ObjType},
- p::Real = 2,
-) where {T,ObjType<:Union{KetQuantumObject,BraQuantumObject}} = normalize(A, p)
-unit(A::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}, p::Real = 1) where {T} = normalize(A, p)
+const vector_to_operator = vec2mat
+const operator_to_vector = mat2vec
@doc raw"""
sqrtm(A::QuantumObject)
Matrix square root of [`Operator`](@ref) type of [`QuantumObject`](@ref)
-Note that for other types of [`QuantumObject`](@ref) use `sprt(A)` instead.
+Note that for other types of [`QuantumObject`](@ref) use `sqrt(A)` instead.
"""
-sqrtm(A::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} = sqrt(A)
+sqrtm(A::QuantumObject{Operator}) = sqrt(A)
@doc raw"""
logm(A::QuantumObject)
Matrix logarithm of [`QuantumObject`](@ref)
-Note that this function is same as `log(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
+Note that this function is same as `log(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref).
"""
-logm(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = log(A)
+logm(A::QuantumObject{ObjType}) where {ObjType<:Union{Operator,SuperOperator}} = log(A)
@doc raw"""
expm(A::QuantumObject)
Matrix exponential of [`QuantumObject`](@ref)
-Note that this function is same as `exp(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
+Note that this function is same as `exp(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref).
"""
-expm(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = exp(A)
+expm(A::QuantumObject{ObjType}) where {ObjType<:Union{Operator,SuperOperator}} = exp(A)
@doc raw"""
sinm(A::QuantumObject)
@@ -134,11 +90,9 @@ Matrix sine of [`QuantumObject`](@ref), defined as
``\sin \left( \hat{A} \right) = \frac{e^{i \hat{A}} - e^{-i \hat{A}}}{2 i}``
-Note that this function is same as `sin(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
+Note that this function is same as `sin(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref).
"""
-sinm(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = sin(A)
+sinm(A::QuantumObject{ObjType}) where {ObjType<:Union{Operator,SuperOperator}} = sin(A)
@doc raw"""
cosm(A::QuantumObject)
@@ -147,104 +101,24 @@ Matrix cosine of [`QuantumObject`](@ref), defined as
``\cos \left( \hat{A} \right) = \frac{e^{i \hat{A}} + e^{-i \hat{A}}}{2}``
-Note that this function is same as `cos(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref)
+Note that this function is same as `cos(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref).
"""
-cosm(
- A::QuantumObject{<:AbstractMatrix{T},ObjType},
-) where {T,ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = cos(A)
+cosm(A::QuantumObject{ObjType}) where {ObjType<:Union{Operator,SuperOperator}} = cos(A)
@doc raw"""
- tensor(A::QuantumObject, B::QuantumObject, ...)
-
-Returns the [Kronecker product](https://en.wikipedia.org/wiki/Kronecker_product) ``\hat{A} \otimes \hat{B} \otimes \cdots``.
-
-Note that this function is same as `kron(A, B, ...)`
-
-# Examples
-
-```
-julia> x = sigmax()
-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> x_list = fill(x, 3);
-
-julia> tensor(x_list...)
-Quantum Object: type=Operator dims=[2, 2, 2] size=(8, 8) ishermitian=true
-8×8 SparseMatrixCSC{ComplexF64, Int64} with 8 stored entries:
- ⋅ ⋅ ⋅ … ⋅ ⋅ 1.0+0.0im
- ⋅ ⋅ ⋅ ⋅ 1.0+0.0im ⋅
- ⋅ ⋅ ⋅ 1.0+0.0im ⋅ ⋅
- ⋅ ⋅ ⋅ ⋅ ⋅ ⋅
- ⋅ ⋅ ⋅ ⋅ ⋅ ⋅
- ⋅ ⋅ 1.0+0.0im … ⋅ ⋅ ⋅
- ⋅ 1.0+0.0im ⋅ ⋅ ⋅ ⋅
- 1.0+0.0im ⋅ ⋅ ⋅ ⋅ ⋅
-```
-"""
-tensor(A::QuantumObject...) = kron(A...)
+ qeye_like(A::AbstractQuantumObject)
-@doc raw"""
- ⊗(A::QuantumObject, B::QuantumObject)
-
-Returns the [Kronecker product](https://en.wikipedia.org/wiki/Kronecker_product) ``\hat{A} \otimes \hat{B}``.
-
-Note that this function is same as `kron(A, B)`
-
-# Examples
-
-```
-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 ⊗ a
-Quantum Object: type=Operator dims=[20, 20] size=(400, 400) ishermitian=false
-400×400 SparseMatrixCSC{ComplexF64, Int64} with 361 stored entries:
-⠀⠀⠘⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀
-⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠦
-```
+Return a similar [`AbstractQuantumObject`](@ref) with `dims` and `type` are same as `A`, but `data` is an identity matrix.
+
+Note that this function is same as `one(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref).
"""
-⊗(A::QuantumObject, B::QuantumObject) = kron(A, B)
+qeye_like(A::AbstractQuantumObject{OpType}) where {OpType<:Union{Operator,SuperOperator}} = one(A)
@doc raw"""
- qeye(N::Int; type=Operator, dims=nothing)
-
-Identity operator ``\hat{\mathbb{1}}`` with size `N`.
+ qzero_like(A::AbstractQuantumObject)
-It is also possible to specify the list of Hilbert dimensions `dims` if different subsystems are present.
+Return a similar [`AbstractQuantumObject`](@ref) with `dims` and `type` are same as `A`, but `data` is a zero-array.
-Note that this function is same as `eye(N, type=type, dims=dims)`, and `type` can only be either [`Operator`](@ref) or [`SuperOperator`](@ref)
+Note that this function is same as `zero(A)` and only supports for [`Operator`](@ref) and [`SuperOperator`](@ref).
"""
-qeye(
- N::Int;
- type::ObjType = Operator,
- dims = nothing,
-) where {ObjType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} =
- QuantumObject(Diagonal(ones(ComplexF64, N)); type = type, dims = dims)
+qzero_like(A::AbstractQuantumObject{OpType}) where {OpType<:Union{Operator,SuperOperator}} = zero(A)
diff --git a/src/settings.jl b/src/settings.jl
new file mode 100644
index 000000000..14f693af8
--- /dev/null
+++ b/src/settings.jl
@@ -0,0 +1,38 @@
+Base.@kwdef mutable struct Settings
+ tidyup_tol::Float64 = 1e-14
+ auto_tidyup::Bool = true
+end
+
+function Base.show(io::IO, s::Settings)
+ # To align the output and make it easier to read
+ # we use rpad `11`, which is the length of string: `auto_tidyup`
+ println(io, "QuantumToolbox.jl Settings")
+ println(io, "--------------------------")
+ map(n -> println(io, rpad("$n", 11, " "), " = ", getfield(s, n)), fieldnames(Settings))
+ return nothing
+end
+
+@doc raw"""
+ QuantumToolbox.settings
+
+Contains all the default global settings of QuantumToolbox.jl.
+
+# List of settings
+
+- `tidyup_tol::Float64 = 1e-14` : tolerance for [`tidyup`](@ref) and [`tidyup!`](@ref).
+- `auto_tidyup::Bool = true` : Automatically tidyup.
+
+For detailed explanation of each settings, see our documentation [here](https://qutip.org/QuantumToolbox.jl/stable/users_guide/settings).
+
+# Change default settings
+
+One can overwrite the default global settings by
+
+```julia
+using QuantumToolbox
+
+QuantumToolbox.settings.tidyup_tol = 1e-10
+QuantumToolbox.settings.auto_tidyup = false
+```
+"""
+const settings = Settings()
diff --git a/src/spectrum.jl b/src/spectrum.jl
new file mode 100644
index 000000000..c9473bc17
--- /dev/null
+++ b/src/spectrum.jl
@@ -0,0 +1,339 @@
+export spectrum, spectrum_correlation_fft
+export SpectrumSolver, ExponentialSeries, PseudoInverse, Lanczos
+
+abstract type SpectrumSolver end
+
+@doc raw"""
+ ExponentialSeries(; tol = 1e-14, calc_steadystate = false)
+
+A solver which solves [`spectrum`](@ref) by finding the eigen decomposition of the Liouvillian [`SuperOperator`](@ref) and calculate the exponential series.
+"""
+struct ExponentialSeries{T<:Real,CALC_SS} <: SpectrumSolver
+ tol::T
+ ExponentialSeries(tol::T, calc_steadystate::Bool = false) where {T} = new{T,calc_steadystate}(tol)
+end
+
+ExponentialSeries(; tol = 1e-14, calc_steadystate = false) = ExponentialSeries(tol, calc_steadystate)
+
+@doc raw"""
+ PseudoInverse(; alg::SciMLLinearSolveAlgorithm = KrylovJL_GMRES())
+
+A solver which solves [`spectrum`](@ref) by finding the inverse of Liouvillian [`SuperOperator`](@ref) using the `alg`orithms given in [`LinearSolve.jl`](https://docs.sciml.ai/LinearSolve/stable/).
+"""
+struct PseudoInverse{MT<:SciMLLinearSolveAlgorithm} <: SpectrumSolver
+ alg::MT
+end
+
+PseudoInverse(; alg::SciMLLinearSolveAlgorithm = KrylovJL_GMRES()) = PseudoInverse(alg)
+
+@doc raw"""
+ Lanczos(; tol = 1e-8, maxiter = 5000, verbose = 0)
+
+A solver which solves [`spectrum`](@ref) by using a non-symmetric Lanczos variant of the algorithm in [Koch2011](https://www.cond-mat.de/events/correl11/manuscripts/koch.pdf).
+The nonsymmetric Lanczos algorithm is adapted from Algorithm 6.6 in [Saad2011](https://www-users.cse.umn.edu/~saad/eig_book_2ndEd.pdf).
+The running estimate is updated via a [Wallis-Euler recursion](https://en.wikipedia.org/wiki/Continued_fraction).
+"""
+Base.@kwdef struct Lanczos{T<:Real,SS<:Union{Nothing,<:SteadyStateSolver}} <: SpectrumSolver
+ tol::T = 1e-8
+ maxiter::Int = 5000
+ verbose::Int = 0
+ steadystate_solver::SS = nothing
+end
+
+@doc raw"""
+ spectrum(H::QuantumObject,
+ ωlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator};
+ solver::SpectrumSolver=ExponentialSeries(),
+ kwargs...)
+
+Calculate the spectrum of the correlation function
+
+```math
+S(\omega) = \int_{-\infty}^\infty \lim_{t \rightarrow \infty} \left\langle \hat{A}(t + \tau) \hat{B}(t) \right\rangle e^{-i \omega \tau} d \tau
+```
+
+See also the following list for `SpectrumSolver` docstrings:
+- [`ExponentialSeries`](@ref)
+- [`PseudoInverse`](@ref)
+- [`Lanczos`](@ref)
+"""
+function spectrum(
+ H::QuantumObject{HOpType},
+ ωlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple},
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator};
+ solver::SpectrumSolver = ExponentialSeries(),
+ kwargs...,
+) where {HOpType<:Union{Operator,SuperOperator}}
+ return _spectrum(liouvillian(H, c_ops), ωlist, A, B, solver; kwargs...)
+end
+
+function _spectrum_get_rates_vecs_ss(L, solver::ExponentialSeries{T,true}) where {T}
+ result = eigen(L)
+ rates, vecs = result.values, result.vectors
+
+ return rates, vecs, steadystate(L).data
+end
+
+function _spectrum_get_rates_vecs_ss(L, solver::ExponentialSeries{T,false}) where {T}
+ result = eigen(L)
+ rates, vecs = result.values, result.vectors
+
+ ss_idx = findmin(abs2, rates)[2]
+ ρss = vec2mat(@view(vecs[:, ss_idx]))
+ ρss = (ρss + ρss') / 2
+ ρss ./= tr(ρss)
+
+ return rates, vecs, ρss
+end
+
+function _spectrum(
+ L::QuantumObject{SuperOperator},
+ ωlist::AbstractVector,
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ solver::ExponentialSeries;
+ kwargs...,
+)
+ check_dimensions(L, A, B)
+
+ rates, vecs, ρss = _spectrum_get_rates_vecs_ss(L, solver)
+
+ ρ0 = B.data * ρss
+ v = vecs \ mat2vec(ρ0)
+
+ amps = map(i -> v[i] * tr(A.data * vec2mat(@view(vecs[:, i]))), eachindex(rates))
+ idxs = findall(x -> abs(x) > solver.tol, amps)
+ amps, rates = amps[idxs], rates[idxs]
+
+ # spec = map(ω -> 2 * real(sum(@. amps * (1 / (1im * ω - rates)))), ωlist)
+ amps_rates = zip(amps, rates)
+ spec = map(ω -> 2 * real(sum(x -> x[1] / (1im * ω - x[2]), amps_rates)), ωlist)
+
+ return spec
+end
+
+function _spectrum(
+ L::QuantumObject{SuperOperator},
+ ωlist::AbstractVector,
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ solver::PseudoInverse;
+ kwargs...,
+)
+ check_dimensions(L, A, B)
+
+ ωList = convert(Vector{_float_type(L)}, ωlist) # Convert it to support GPUs and avoid type instabilities
+ Length = length(ωList)
+ spec = Vector{_float_type(L)}(undef, Length)
+
+ # calculate vectorized steadystate, multiply by operator B on the left (spre)
+ ρss = mat2vec(steadystate(L))
+ b = (spre(B) * ρss).data
+
+ # multiply by operator A on the left (spre) and then perform trace operation
+ D = prod(L.dimensions)
+ _tr = SparseVector(D^2, [1 + n * (D + 1) for n in 0:(D-1)], ones(_complex_float_type(L), D)) # same as vec(system_identity_matrix)
+ _tr_A = transpose(_tr) * spre(A).data
+
+ Id = I(D^2)
+
+ # DO the idx = 1 case
+ ω = ωList[1]
+ cache = init(LinearProblem(L.data - 1im * ω * Id, b), solver.alg, kwargs...)
+ sol = solve!(cache)
+ spec[1] = -2 * real(dot(_tr_A, sol.u))
+ popfirst!(ωList)
+ for (idx, ω) in enumerate(ωList)
+ cache.A = L.data - 1im * ω * Id
+ sol = solve!(cache)
+
+ # trace over the Hilbert space of system (expectation value)
+ spec[idx+1] = -2 * real(dot(_tr_A, sol.u))
+ end
+
+ return spec
+end
+
+function _spectrum(
+ L::QuantumObject{SuperOperator},
+ ωlist::AbstractVector,
+ A::QuantumObject{Operator},
+ B::QuantumObject{Operator},
+ solver::Lanczos,
+)
+ check_dimensions(L, A, B)
+
+ # Define type shortcuts
+ fT = _float_type(L)
+ cT = _complex_float_type(L)
+
+ # Calculate |v₁> = B|ρss>
+ ρss =
+ isnothing(solver.steadystate_solver) ? mat2vec(steadystate(L)) :
+ mat2vec(steadystate(L; solver = solver.steadystate_solver))
+ vₖ = (spre(B) * ρss).data
+
+ # Define (possibly GPU) vector type
+ vT = typeof(vₖ)
+
+ # Calculate and max(abs(x), abs(y)), maxNorm, Aₖ, Bₖ)
+ Aₖ ./= maxNorm
+ Bₖ ./= maxNorm
+ A₋₁ ./= maxNorm
+ B₋₁ ./= maxNorm
+ end
+
+ # Check for convergence
+
+ residueNorm = max(maximum(abs, lanczosFactor), maximum(abs, lanczosFactor₋₁))
+ lanczosFactor₋₁ .-= lanczosFactor
+ maxResidue = maximum(abs, lanczosFactor₋₁) / residueNorm
+ if maxResidue <= solver.tol
+ if solver.verbose > 1
+ println("spectrum(): solver::Lanczos converged after $(k) iterations")
+ end
+ break
+ end
+
+ # (k+1)-th left/right vectors, orthogonal to previous ones
+ mul!(v₊₁, L.data, vₖ)
+ v₊₁ .= v₊₁ .- αₖ .* vₖ .- βₖ .* v₋₁
+ w₊₁ .= w₊₁ .- conj(αₖ) .* wₖ .- conj(δₖ) .* w₋₁
+ v₋₁, vₖ = vₖ, v₋₁
+ vₖ, v₊₁ = v₊₁, vₖ
+ w₋₁, wₖ = wₖ, w₋₁
+ wₖ, w₊₁ = w₊₁, wₖ
+
+ # k-th off-diagonal elements
+ βₖδₖ = dot(wₖ, vₖ)
+ if βₖδₖ ≈ 0.0
+ if solver.verbose > 0
+ @warn "spectrum(): solver::Lanczos experienced orthogonality breakdown after $(k) iterations"
+ @warn "spectrum(): βₖδₖ = $(βₖδₖ)"
+ end
+ break
+ end
+ δₖ = sqrt(abs(βₖδₖ))
+ βₖ = βₖδₖ / δₖ
+
+ # Normalize (k+1)-th left/right vectors
+ vₖ ./= δₖ
+ wₖ ./= conj(βₖ)
+
+ # Update everything for the next cycle
+ A₋₂, A₋₁ = A₋₁, A₋₂
+ A₋₁, Aₖ = Aₖ, A₋₁
+ B₋₂, B₋₁ = B₋₁, B₋₂
+ B₋₁, Bₖ = Bₖ, B₋₁
+ end
+
+ if solver.verbose > 0 && maxResidue > solver.tol
+ println("spectrum(): maxiter = $(solver.maxiter) reached before convergence!")
+ println("spectrum(): Max residue = $maxResidue")
+ println("spectrum(): Consider increasing maxiter and/or tol")
+ end
+
+ # Restore the norm
+ lanczosFactor .= gfNorm .* lanczosFactor
+
+ return -2 .* real(lanczosFactor)
+end
+
+@doc raw"""
+ spectrum_correlation_fft(tlist, corr; inverse=false)
+
+Calculate the power spectrum corresponding to a two-time correlation function using fast Fourier transform (FFT).
+
+# Parameters
+- `tlist::AbstractVector`: List of time points at which the two-time correlation function is given.
+- `corr::AbstractVector`: List of two-time correlations corresponding to the given time point in `tlist`.
+- `inverse::Bool`: Whether to use the inverse Fourier transform or not. Default to `false`.
+
+# Returns
+- `ωlist`: the list of angular frequencies ``\omega``.
+- `Slist`: the list of the power spectrum corresponding to the angular frequencies in `ωlist`.
+"""
+function spectrum_correlation_fft(tlist::AbstractVector, corr::AbstractVector; inverse::Bool = false)
+ N = length(tlist)
+ dt_list = diff(tlist)
+ dt = dt_list[1]
+
+ all(≈(dt), dt_list) || throw(ArgumentError("tlist must be equally spaced for FFT."))
+
+ # power spectrum list
+ F = inverse ? N * ifft(corr) : fft(corr)
+ Slist = 2 * dt * real(fftshift(F))
+
+ # angular frequency list
+ ωlist = 2 * π * fftshift(fftfreq(N, 1 / dt))
+
+ return ωlist, Slist
+end
diff --git a/src/spin_lattice.jl b/src/spin_lattice.jl
index 26aa48ae0..f3f47b237 100644
--- a/src/spin_lattice.jl
+++ b/src/spin_lattice.jl
@@ -1,12 +1,10 @@
-export Lattice, mb, TFIM, nn, sx, sy, sz, sm, sp, pbc, obc
+export Lattice, multisite_operator, DissipativeIsing
-sx = sigmax()
-sy = -sigmay()
-sz = -sigmaz()
-sm = (sx - 1im * sy) / 2
-sp = (sx + 1im * sy) / 2
+@doc raw"""
+ Lattice
-#Lattice structure
+A Julia constructor for a lattice object. The lattice object is used to define the geometry of the lattice. `Nx` and `Ny` are the number of sites in the x and y directions, respectively. `N` is the total number of sites. `lin_idx` is a `LinearIndices` object and `car_idx` is a `CartesianIndices` object, and they are used to efficiently select sites on the lattice.
+"""
Base.@kwdef struct Lattice{TN<:Integer,TLI<:LinearIndices,TCI<:CartesianIndices}
Nx::TN
Ny::TN
@@ -16,47 +14,147 @@ Base.@kwdef struct Lattice{TN<:Integer,TLI<:LinearIndices,TCI<:CartesianIndices}
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 = ntuple(j -> 2, Val(N)))
+@doc raw"""
+ multisite_operator(dims::Union{AbstractVector, Tuple}, pairs::Pair{<:Integer,<:QuantumObject}...)
+
+A Julia function for generating a multi-site operator ``\\hat{O} = \\hat{O}_i \\hat{O}_j \\cdots \\hat{O}_k``. The function takes a vector of dimensions `dims` and a list of pairs `pairs` where the first element of the pair is the site index and the second element is the operator acting on that site.
+
+# Arguments
+- `dims::Union{AbstractVector, Tuple}`: A vector of dimensions of the lattice.
+- `pairs::Pair{<:Integer,<:QuantumObject}...`: A list of pairs where the first element of the pair is the site index and the second element is the operator acting on that site.
+
+# Returns
+`QuantumObject`: A `QuantumObject` representing the multi-site operator.
+
+# Example
+```jldoctest
+julia> op = multisite_operator(Val(8), 5=>sigmax(), 7=>sigmaz());
+
+julia> op.dims
+8-element StaticArraysCore.SVector{8, Int64} with indices SOneTo(8):
+ 2
+ 2
+ 2
+ 2
+ 2
+ 2
+ 2
+ 2
+```
+"""
+function multisite_operator(dims::Union{AbstractVector,Tuple}, pairs::Pair{<:Integer,<:QuantumObject}...)
+ sites_unsorted = collect(first.(pairs))
+ idxs = sortperm(sites_unsorted)
+ _sites = sites_unsorted[idxs]
+ _ops = collect(last.(pairs))[idxs]
+ _dims = collect(dims) # Use this instead of a Tuple, to avoid type instability when indexing on a slice
+
+ sites, ops = _get_unique_sites_ops(_sites, _ops)
+
+ _dims[sites] == [get_dimensions_to(op)[1].size for op in ops] || throw(ArgumentError("The dimensions of the operators do not match the dimensions of the lattice."))
+
+ data = kron(I(prod(_dims[1:(sites[1]-1)])), ops[1].data)
+ for i in 2:length(sites)
+ data = kron(data, I(prod(_dims[(sites[i-1]+1):(sites[i]-1)])), ops[i].data)
+ end
+ data = kron(data, I(prod(_dims[(sites[end]+1):end])))
+
+ return QuantumObject(data; type = Operator(), dims = dims)
+end
+function multisite_operator(N::Union{Integer,Val}, pairs::Pair{<:Integer,<:QuantumObject}...)
+ dims = ntuple(j -> 2, makeVal(N))
+
+ return multisite_operator(dims, pairs...)
+end
+function multisite_operator(latt::Lattice, pairs::Pair{<:Integer,<:QuantumObject}...)
+ return multisite_operator(makeVal(latt.N), pairs...)
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} =
- mb(s, latt.idx[row, col], latt.N)
-mb(s::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, x::CartesianIndex, latt::Lattice) where {T1} =
- mb(s, latt.idx[x], latt.N)
#Definition of nearest-neighbour sites on lattice
-pbc(i::Integer, N::Integer) = 1 + (i - 1 + N) % N
-obc(i::Integer, N::Integer) = (i >= 1 && i <= N)
-pbc(i::Vector{Int}, N::Integer) = pbc.(i, N)
-obc(i::Vector{Int}, N::Integer) = filter(x -> obc(x, N), i)
-
-function nn(i::CartesianIndex, latt::Lattice, bc::Function; order::Integer = 1)
- row = bc([i[1] + order, i[1] - order], latt.Nx)
- col = bc([i[2] + order, i[2] - order], latt.Ny)
+periodic_boundary_conditions(i::Integer, N::Integer) = 1 + (i - 1 + N) % N
+open_boundary_conditions(i::Integer, N::Integer) = (i >= 1 && i <= N)
+periodic_boundary_conditions(i::Vector{Int}, N::Integer) = periodic_boundary_conditions.(i, N)
+open_boundary_conditions(i::Vector{Int}, N::Integer) = filter(x -> open_boundary_conditions(x, N), i)
+
+function nearest_neighbor(i::CartesianIndex, latt::Lattice, ::Val{:periodic_bc}; order::Integer = 1)
+ row = periodic_boundary_conditions([i[1] + order, i[1] - order], latt.Nx)
+ col = periodic_boundary_conditions([i[2] + order, i[2] - order], latt.Ny)
return vcat([CartesianIndex(r, i[2]) for r in row], [CartesianIndex(i[1], c) for c in col])
end
-function TFIM(Jx::Real, Jy::Real, Jz::Real, hx::Real, γ::Real, latt::Lattice; bc::Function = pbc, order::Integer = 1)
- S = [mb(sm, i, latt) for i in 1:latt.N]
+function nearest_neighbor(i::CartesianIndex, latt::Lattice, ::Val{:open_bc}; order::Integer = 1)
+ row = periodic_boundary_conditions([i[1] + order, i[1] - order], latt.Nx)
+ col = periodic_boundary_conditions([i[2] + order, i[2] - order], latt.Ny)
+ return vcat([CartesianIndex(r, i[2]) for r in row], [CartesianIndex(i[1], c) for c in col])
+end
+
+@doc """
+ DissipativeIsing(Jx::Real, Jy::Real, Jz::Real, hx::Real, hy::Real, hz::Real, γ::Real, latt::Lattice; boundary_condition::Union{Symbol, Val} = Val(:periodic_bc), order::Integer = 1)
+
+A Julia constructor for a dissipative Ising model. The function returns the Hamiltonian
+
+```math
+\\hat{H} = \\frac{J_x}{2} \\sum_{\\langle i, j \\rangle} \\hat{\\sigma}_i^x \\hat{\\sigma}_j^x + \\frac{J_y}{2} \\sum_{\\langle i, j \\rangle} \\hat{\\sigma}_i^y \\hat{\\sigma}_j^y + \\frac{J_z}{2} \\sum_{\\langle i, j \\rangle} \\hat{\\sigma}_i^z \\hat{\\sigma}_j^z + h_x \\sum_i \\hat{\\sigma}_i^x
+```
+
+and the collapse operators
+
+```math
+\\hat{c}_i = \\sqrt{\\gamma} \\hat{\\sigma}_i^-
+```
+
+# Arguments
+- `Jx::Real`: The coupling constant in the x-direction.
+- `Jy::Real`: The coupling constant in the y-direction.
+- `Jz::Real`: The coupling constant in the z-direction.
+- `hx::Real`: The magnetic field in the x-direction.
+- `hy::Real`: The magnetic field in the y-direction.
+- `hz::Real`: The magnetic field in the z-direction.
+- `γ::Real`: The local dissipation rate.
+- `latt::Lattice`: A [`Lattice`](@ref) object that defines the geometry of the lattice.
+- `boundary_condition::Union{Symbol, Val}`: The boundary conditions of the lattice. The possible inputs are `periodic_bc` and `open_bc`, for periodic or open boundary conditions, respectively. The default value is `Val(:periodic_bc)`.
+- `order::Integer`: The order of the nearest-neighbour sites. The default value is 1.
+"""
+function DissipativeIsing(
+ Jx::Real,
+ Jy::Real,
+ Jz::Real,
+ hx::Real,
+ hy::Real,
+ hz::Real,
+ γ::Real,
+ latt::Lattice;
+ boundary_condition::Union{Symbol,Val} = Val(:periodic_bc),
+ order::Integer = 1,
+)
+ S = [multisite_operator(latt, i => sigmam()) for i in 1:latt.N]
c_ops = sqrt(γ) .* S
- op_sum(S, 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[nearest_neighbor(i, latt, makeVal(boundary_condition); order = order)]])
H = 0
if (Jx != 0 || hx != 0)
- S .= [mb(sx, i, latt) for i in 1:latt.N]
+ S = [multisite_operator(latt, i => sigmax()) for i in 1:latt.N]
H += Jx / 2 * mapreduce(i -> op_sum(S, i), +, latt.car_idx) #/2 because we are double counting
H += hx * sum(S)
end
- if Jy != 0
- S .= [mb(sy, i, latt) for i in 1:latt.N]
+ if (Jy != 0 || hy != 0)
+ S = [multisite_operator(latt, i => sigmay()) for i in 1:latt.N]
H += Jy / 2 * mapreduce(i -> op_sum(S, i), +, latt.car_idx)
+ H += hy * sum(S)
end
- if Jz != 0
- S .= [mb(sz, i, latt) for i in 1:latt.N]
+ if (Jz != 0 || hz != 0)
+ S = [multisite_operator(latt, i => sigmaz()) for i in 1:latt.N]
H += Jz / 2 * mapreduce(i -> op_sum(S, i), +, latt.car_idx)
+ H += hz * sum(S)
end
return H, c_ops
-end;
+end
+
+function _get_unique_sites_ops(sites, ops)
+ unique_sites = unique(sites)
+ unique_ops = map(i -> prod(ops[findall(==(i), sites)]), unique_sites)
+
+ return unique_sites, unique_ops
+end
diff --git a/src/steadystate.jl b/src/steadystate.jl
index 7cdd0f144..49ce2f44c 100644
--- a/src/steadystate.jl
+++ b/src/steadystate.jl
@@ -1,36 +1,37 @@
-export steadystate, steadystate_floquet
+export steadystate, steadystate_fourier, steadystate_floquet
export SteadyStateSolver,
- SteadyStateODESolver,
- SteadyStateLinearSolver,
- SteadyStateEigenSolver,
SteadyStateDirectSolver,
- SteadyStateFloquetSolver,
- SSFloquetLinearSystem,
+ SteadyStateEigenSolver,
+ SteadyStateLinearSolver,
+ SteadyStateODESolver,
SSFloquetEffectiveLiouvillian
abstract type SteadyStateSolver end
-abstract type SteadyStateFloquetSolver end
@doc raw"""
- SteadyStateODESolver{::OrdinaryDiffEqAlgorithm}
+ SteadyStateDirectSolver()
-A structure representing an ordinary differential equation (ODE) solver for solving [`steadystate`](@ref)
+A solver which solves [`steadystate`](@ref) by finding the inverse of Liouvillian [`SuperOperator`](@ref) using the standard method given in `LinearAlgebra`.
+"""
+struct SteadyStateDirectSolver <: SteadyStateSolver end
-It includes a field (attribute) `SteadyStateODESolver.alg` that specifies the solving algorithm. Default to `Tsit5()`.
+@doc raw"""
+ SteadyStateEigenSolver()
-For more details about the solvers, please refer to [`OrdinaryDiffEq.jl`](https://docs.sciml.ai/OrdinaryDiffEq/stable/)
+A solver which solves [`steadystate`](@ref) by finding the zero (or lowest) eigenvalue of Liouvillian [`SuperOperator`](@ref) using [`eigsolve`](@ref).
"""
-Base.@kwdef struct SteadyStateODESolver{MT<:OrdinaryDiffEqAlgorithm} <: SteadyStateSolver
- alg::MT = Tsit5()
-end
+struct SteadyStateEigenSolver <: SteadyStateSolver end
-struct SSFloquetLinearSystem <: SteadyStateFloquetSolver end
-Base.@kwdef struct SSFloquetEffectiveLiouvillian{SSST<:SteadyStateSolver} <: SteadyStateFloquetSolver
- steadystate_solver::SSST = SteadyStateDirectSolver()
-end
+@doc raw"""
+ SteadyStateLinearSolver(alg = KrylovJL_GMRES(), Pl = nothing, Pr = nothing)
-struct SteadyStateDirectSolver <: SteadyStateSolver end
-struct SteadyStateEigenSolver <: SteadyStateSolver end
+A solver which solves [`steadystate`](@ref) by finding the inverse of Liouvillian [`SuperOperator`](@ref) using the `alg`orithms given in [`LinearSolve.jl`](https://docs.sciml.ai/LinearSolve/stable/).
+
+# Arguments
+- `alg::SciMLLinearSolveAlgorithm=KrylovJL_GMRES()`: algorithms given in [`LinearSolve.jl`](https://docs.sciml.ai/LinearSolve/stable/)
+- `Pl::Union{Function,Nothing}=nothing`: left preconditioner, see documentation [Solving for Steady-State Solutions](@ref doc:Solving-for-Steady-State-Solutions) for more details.
+- `Pr::Union{Function,Nothing}=nothing`: right preconditioner, see documentation [Solving for Steady-State Solutions](@ref doc:Solving-for-Steady-State-Solutions) for more details.
+"""
Base.@kwdef struct SteadyStateLinearSolver{
MT<:Union{SciMLLinearSolveAlgorithm,Nothing},
PlT<:Union{Function,Nothing},
@@ -42,121 +43,119 @@ Base.@kwdef struct SteadyStateLinearSolver{
end
@doc raw"""
- steadystate(
- H::QuantumObject{MT1,HOpType},
- ψ0::QuantumObject{<:AbstractArray{T2},StateOpType},
- tspan::Real = Inf,
- c_ops::Union{Nothing,AbstractVector} = nothing;
- solver::SteadyStateODESolver = SteadyStateODESolver(),
- reltol::Real = 1.0e-8,
- abstol::Real = 1.0e-10,
- kwargs...,
+ SteadyStateODESolver(
+ alg = Tsit5(),
+ ψ0 = nothing,
+ tmax = Inf,
+ terminate_reltol = 1e-5,
+ terminate_abstol = 1e-7
)
-Solve the stationary state based on time evolution (ordinary differential equations; `OrdinaryDiffEq.jl`) with a given initial state.
+An ordinary differential equation (ODE) solver for solving [`steadystate`](@ref). It solves the stationary state based on [`mesolve`](@ref) with a termination condition.
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::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.
-- `kwargs...`: The keyword arguments for the ODEProblem.
-"""
-function steadystate(
- H::QuantumObject{MT1,HOpType},
- ψ0::QuantumObject{<:AbstractArray{T2},StateOpType},
- tspan::Real = Inf,
- c_ops::Union{Nothing,AbstractVector} = nothing;
- solver::SteadyStateODESolver = SteadyStateODESolver(),
- reltol::Real = 1.0e-8,
- abstol::Real = 1.0e-10,
- kwargs...,
-) where {
- MT1<:AbstractMatrix,
- T2,
- HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},
-}
- (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)
- L = MatrixOperator(liouvillian(H, c_ops).data)
-
- prob = ODEProblem{true}(L, u0, (0.0, tspan))
- sol = solve(
- prob,
- solver.alg;
- callback = TerminateSteadyState(abstol, reltol, _steadystate_ode_condition),
- reltol = reltol,
- abstol = abstol,
- kwargs...,
- )
+# Arguments
+- `alg::OrdinaryDiffEqAlgorithm=Tsit5()`: The algorithm to solve the ODE.
+- `ψ0::Union{Nothing,QuantumObject}=nothing`: The initial state of the system. If not specified, a random pure state will be generated.
+- `tmax::Real=Inf`: The final time step for the steady state problem.
+- `terminate_reltol` = The relative tolerance for stationary state terminate condition. Default to `1e-5`.
+- `terminate_abstol` = The absolute tolerance for stationary state terminate condition. Default to `1e-7`.
- ρss = reshape(sol.u[end], N, N)
- ρss = (ρss + ρss') / 2 # Hermitianize
- return QuantumObject(ρss, Operator, H.dims)
+!!! warning "Tolerances for terminate condition"
+ The terminate condition tolerances `terminate_reltol` and `terminate_abstol` should be larger than `reltol` and `abstol` of [`mesolve`](@ref), respectively.
+
+For more details about the solving `alg`orithms, please refer to [`OrdinaryDiffEq.jl`](https://docs.sciml.ai/OrdinaryDiffEq/stable/).
+"""
+Base.@kwdef struct SteadyStateODESolver{
+ MT<:OrdinaryDiffEqAlgorithm,
+ ST<:Union{Nothing,QuantumObject},
+ TT<:Real,
+ RT<:Real,
+ AT<:Real,
+} <: SteadyStateSolver
+ alg::MT = Tsit5()
+ ψ0::ST = nothing
+ tmax::TT = Inf
+ terminate_reltol::RT = 10 * DEFAULT_ODE_SOLVER_OPTIONS.reltol
+ terminate_abstol::AT = 10 * DEFAULT_ODE_SOLVER_OPTIONS.abstol
end
-function _steadystate_ode_condition(integrator, abstol, reltol, min_t)
- # this condition is same as DiffEqBase.NormTerminationMode
+@doc raw"""
+ SSFloquetEffectiveLiouvillian(steadystate_solver = SteadyStateDirectSolver())
- du_dt = (integrator.u - integrator.uprev) / integrator.dt
- norm_du_dt = norm(du_dt)
- if (norm_du_dt <= reltol * norm(du_dt + integrator.u)) || (norm_du_dt <= abstol)
- return true
- else
- return false
- end
+A solver which solves [`steadystate_fourier`](@ref) by first extracting an effective time-independent Liouvillian and then using the `steadystate_solver` to extract the steadystate..
+
+# Parameters
+- `steadystate_solver::SteadyStateSolver=SteadyStateDirectSolver()`: The solver to use for the effective Liouvillian.
+
+!!! note
+ This solver is only available for [`steadystate_fourier`](@ref).
+"""
+Base.@kwdef struct SSFloquetEffectiveLiouvillian{SSST<:SteadyStateSolver} <: SteadyStateSolver
+ steadystate_solver::SSST = SteadyStateDirectSolver()
end
+@doc raw"""
+ steadystate(
+ H::AbstractQuantumObject{OpType},
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ solver::SteadyStateSolver = SteadyStateDirectSolver(),
+ kwargs...,
+ )
+
+Solve the stationary state based on different solvers.
+
+# Parameters
+- `H`: The Hamiltonian or the Liouvillian of the system.
+- `c_ops`: The list of the collapse operators.
+- `solver`: see documentation [Solving for Steady-State Solutions](@ref doc:Solving-for-Steady-State-Solutions) for different solvers.
+- `kwargs`: The keyword arguments for the solver.
+"""
function steadystate(
- H::QuantumObject{<:AbstractArray,OpType},
- c_ops::Union{Nothing,AbstractVector} = nothing;
+ H::AbstractQuantumObject{OpType},
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
solver::SteadyStateSolver = SteadyStateDirectSolver(),
kwargs...,
-) where {OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
+) where {OpType<:Union{Operator,SuperOperator}}
+ solver isa SSFloquetEffectiveLiouvillian && throw(
+ ArgumentError(
+ "The solver `SSFloquetEffectiveLiouvillian` is only available for the `steadystate_fourier` function.",
+ ),
+ )
+
L = liouvillian(H, c_ops)
return _steadystate(L, solver; kwargs...)
end
-function _steadystate(
- L::QuantumObject{<:AbstractArray{T},SuperOperatorQuantumObject},
- solver::SteadyStateLinearSolver;
- kwargs...,
-) where {T}
+function _steadystate(L::QuantumObject{SuperOperator}, solver::SteadyStateLinearSolver; kwargs...)
L_tmp = L.data
- N = prod(L.dims)
+ N = prod(L.dimensions)
weight = norm(L_tmp, 1) / length(L_tmp)
- v0 = _get_dense_similar(L_tmp, N^2)
+ v0 = _dense_similar(L_tmp, N^2)
fill!(v0, 0)
allowed_setindex!(v0, weight, 1) # Because scalar indexing is not allowed on GPU arrays
idx_range = collect(1:N)
- rows = _get_dense_similar(L_tmp, N)
- cols = _get_dense_similar(L_tmp, N)
- datas = _get_dense_similar(L_tmp, N)
+ rows = _dense_similar(L_tmp, N)
+ cols = _dense_similar(L_tmp, N)
+ vals = _dense_similar(L_tmp, N)
fill!(rows, 1)
copyto!(cols, N .* (idx_range .- 1) .+ idx_range)
- fill!(datas, weight)
- Tn = sparse(rows, cols, datas, N^2, N^2)
+ fill!(vals, weight)
+ Tn = _sparse_similar(L_tmp, rows, cols, vals, N^2, N^2)
L_tmp = L_tmp + Tn
(haskey(kwargs, :Pl) || haskey(kwargs, :Pr)) && error("The use of preconditioners must be defined in the solver.")
@@ -172,63 +171,107 @@ function _steadystate(
ρss = reshape(ρss_vec, N, N)
ρss = (ρss + ρss') / 2 # Hermitianize
- return QuantumObject(ρss, Operator, L.dims)
+ return QuantumObject(ρss, Operator(), L.dimensions)
end
-function _steadystate(
- L::QuantumObject{<:AbstractArray{T},SuperOperatorQuantumObject},
- solver::SteadyStateEigenSolver;
- kwargs...,
-) where {T}
- N = prod(L.dims)
+function _steadystate(L::QuantumObject{SuperOperator}, solver::SteadyStateEigenSolver; kwargs...)
+ N = prod(L.dimensions)
- kwargs = merge((sigma = 1e-8, k = 1), (; kwargs...))
+ kwargs = merge((sigma = 1e-8, eigvals = 1), (; kwargs...))
ρss_vec = eigsolve(L; kwargs...).vectors[:, 1]
ρss = reshape(ρss_vec, N, N)
ρss /= tr(ρss)
ρss = (ρss + ρss') / 2 # Hermitianize
- return QuantumObject(ρss, Operator, L.dims)
+ return QuantumObject(ρss, Operator(), L.dimensions)
end
-function _steadystate(
- L::QuantumObject{<:AbstractArray{T},SuperOperatorQuantumObject},
- solver::SteadyStateDirectSolver,
-) where {T}
+function _steadystate(L::QuantumObject{SuperOperator}, solver::SteadyStateDirectSolver)
L_tmp = L.data
- N = prod(L.dims)
+ N = prod(L.dimensions)
weight = norm(L_tmp, 1) / length(L_tmp)
- v0 = _get_dense_similar(L_tmp, N^2)
+ v0 = _dense_similar(L_tmp, N^2)
fill!(v0, 0)
allowed_setindex!(v0, weight, 1) # Because scalar indexing is not allowed on GPU arrays
idx_range = collect(1:N)
- rows = _get_dense_similar(L_tmp, N)
- cols = _get_dense_similar(L_tmp, N)
- datas = _get_dense_similar(L_tmp, N)
+ rows = _dense_similar(L_tmp, N)
+ cols = _dense_similar(L_tmp, N)
+ vals = _dense_similar(L_tmp, N)
fill!(rows, 1)
copyto!(cols, N .* (idx_range .- 1) .+ idx_range)
- fill!(datas, weight)
- Tn = sparse(rows, cols, datas, N^2, N^2)
+ fill!(vals, weight)
+ Tn = sparse(rows, cols, vals, N^2, N^2)
L_tmp = L_tmp + Tn
ρss_vec = L_tmp \ v0 # This is still not supported on GPU, yet
ρss = reshape(ρss_vec, N, N)
ρss = (ρss + ρss') / 2 # Hermitianize
- return QuantumObject(ρss, Operator, L.dims)
+ return QuantumObject(ρss, Operator(), L.dimensions)
+end
+
+function _steadystate(L::AbstractQuantumObject{SuperOperator}, solver::SteadyStateODESolver; kwargs...)
+ ψ0 = isnothing(solver.ψ0) ? rand_ket(L.dimensions) : solver.ψ0
+ ftype = _float_type(ψ0)
+ tlist = [ftype(0), ftype(solver.tmax)]
+
+ # overwrite some kwargs and throw warning message to tell the users that we are ignoring these settings
+ haskey(kwargs, :progress_bar) && @warn "Ignore keyword argument 'progress_bar' for SteadyStateODESolver"
+ haskey(kwargs, :save_everystep) && @warn "Ignore keyword argument 'save_everystep' for SteadyStateODESolver"
+ haskey(kwargs, :saveat) && @warn "Ignore keyword argument 'saveat' for SteadyStateODESolver"
+ kwargs2 = merge(
+ NamedTuple(kwargs), # we convert to NamedTuple just in case if kwargs is empty
+ (progress_bar = Val(false), save_everystep = false, saveat = ftype[]),
+ )
+
+ # add terminate condition (callback)
+ cb = TerminateSteadyState(
+ solver.terminate_abstol,
+ solver.terminate_reltol,
+ SteadyStateODECondition(similar(mat2vec(ket2dm(ψ0)).data)),
+ )
+ kwargs3 = _merge_kwargs_with_callback(kwargs2, cb)
+
+ sol = mesolve(L, ψ0, tlist; kwargs3...)
+ ρss = sol.states[end]
+ return ρss
+end
+
+_steadystate(
+ L::QuantumObjectEvolution{SuperOperator},
+ solver::T;
+ kwargs...,
+) where {T<:Union{SteadyStateDirectSolver,SteadyStateEigenSolver,SteadyStateLinearSolver}} =
+ throw(ArgumentError("$(get_typename_wrapper(solver)) does not support QobjEvo."))
+
+struct SteadyStateODECondition{CT<:AbstractArray}
+ cache::CT
+end
+
+function (f::SteadyStateODECondition)(integrator, abstol, reltol, min_t)
+ # this condition is same as DiffEqBase.NormTerminationMode
+
+ f.cache .= (integrator.u .- integrator.uprev) ./ integrator.dt
+ norm_du_dt = norm(f.cache)
+ f.cache .+= integrator.u
+ if norm_du_dt <= reltol * norm(f.cache) || norm_du_dt <= abstol
+ return true
+ else
+ return false
+ end
end
@doc raw"""
- steadystate_floquet(
- H_0::QuantumObject{MT,OpType1},
- H_p::QuantumObject{<:AbstractArray,OpType2},
- H_m::QuantumObject{<:AbstractArray,OpType3},
+ steadystate_fourier(
+ H_0::QuantumObject,
+ H_p::QuantumObject,
+ H_m::QuantumObject,
ωd::Number,
- c_ops::Union{Nothing,AbstractVector} = nothing;
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
n_max::Integer = 2,
tol::R = 1e-8,
- solver::FSolver = SSFloquetLinearSystem,
+ solver::FSolver = SteadyStateLinearSolver(),
kwargs...,
)
@@ -238,11 +281,11 @@ Considering a monochromatic drive at frequency ``\omega_d``, we divide it into t
`H_p` and `H_m`, where `H_p` oscillates
as ``e^{i \omega t}`` and `H_m` oscillates as ``e^{-i \omega t}``.
There are two solvers available for this function:
-- `SSFloquetLinearSystem`: Solves the linear system of equations.
+- `SteadyStateLinearSolver`: Solves the linear system of equations.
- `SSFloquetEffectiveLiouvillian`: Solves the effective Liouvillian.
For both cases, `n_max` is the number of Fourier components to consider, and `tol` is the tolerance for the solver.
-In the case of `SSFloquetLinearSystem`, the full linear system is solved at once:
+In the case of `SteadyStateLinearSolver`, the full linear system is solved at once:
```math
( \mathcal{L}_0 - i n \omega_d ) \hat{\rho}_n + \mathcal{L}_1 \hat{\rho}_{n-1} + \mathcal{L}_{-1} \hat{\rho}_{n+1} = 0
@@ -285,53 +328,58 @@ This will allow to simultaneously obtain all the ``\hat{\rho}_n``.
In the case of `SSFloquetEffectiveLiouvillian`, instead, the effective Liouvillian is calculated using the matrix continued fraction method.
!!! note "different return"
- The two solvers returns different objects. The `SSFloquetLinearSystem` returns a list of [`QuantumObject`](@ref), containing the density matrices for each Fourier component (``\hat{\rho}_{-n}``, with ``n`` from ``0`` to ``n_\textrm{max}``), while the `SSFloquetEffectiveLiouvillian` returns only ``\hat{\rho}_0``.
+ The two solvers returns different objects. The `SteadyStateLinearSolver` returns a list of [`QuantumObject`](@ref), containing the density matrices for each Fourier component (``\hat{\rho}_{-n}``, with ``n`` from ``0`` to ``n_\textrm{max}``), while the `SSFloquetEffectiveLiouvillian` returns only ``\hat{\rho}_0``.
+
+!!! note
+ `steadystate_floquet` is a synonym of `steadystate_fourier`.
## Arguments
- `H_0::QuantumObject`: The Hamiltonian or the Liouvillian of the undriven system.
- `H_p::QuantumObject`: The Hamiltonian or the Liouvillian of the part of the drive that oscillates as ``e^{i \omega t}``.
- `H_m::QuantumObject`: The Hamiltonian or the Liouvillian of the part of the drive that oscillates as ``e^{-i \omega t}``.
- `ωd::Number`: The frequency of the drive.
-- `c_ops::AbstractVector = QuantumObject`: The optional collapse operators.
+- `c_ops::Union{Nothing,AbstractVector} = nothing`: The optional collapse operators.
- `n_max::Integer = 2`: The number of Fourier components to consider.
- `tol::R = 1e-8`: The tolerance for the solver.
-- `solver::FSolver = SSFloquetLinearSystem`: The solver to use.
+- `solver::FSolver = SteadyStateLinearSolver`: The solver to use.
- `kwargs...`: Additional keyword arguments to be passed to the solver.
"""
-function steadystate_floquet(
- H_0::QuantumObject{MT,OpType1},
- H_p::QuantumObject{<:AbstractArray,OpType2},
- H_m::QuantumObject{<:AbstractArray,OpType3},
+function steadystate_fourier(
+ H_0::QuantumObject{OpType1},
+ H_p::QuantumObject{OpType2},
+ H_m::QuantumObject{OpType3},
ωd::Number,
- c_ops::Union{Nothing,AbstractVector} = nothing;
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
n_max::Integer = 2,
tol::R = 1e-8,
- solver::FSolver = SSFloquetLinearSystem(),
+ solver::FSolver = SteadyStateLinearSolver(),
kwargs...,
) where {
- MT<:AbstractArray,
- OpType1<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- OpType2<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- OpType3<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
+ OpType1<:Union{Operator,SuperOperator},
+ OpType2<:Union{Operator,SuperOperator},
+ OpType3<:Union{Operator,SuperOperator},
R<:Real,
- FSolver<:SteadyStateFloquetSolver,
+ FSolver<:SteadyStateSolver,
}
L_0 = liouvillian(H_0, c_ops)
L_p = liouvillian(H_p)
L_m = liouvillian(H_m)
- return _steadystate_floquet(L_0, L_p, L_m, ωd, solver; n_max = n_max, tol = tol, kwargs...)
+ return _steadystate_fourier(L_0, L_p, L_m, ωd, solver; n_max = n_max, tol = tol, kwargs...)
end
-function _steadystate_floquet(
- L_0::QuantumObject{<:AbstractArray{T1},SuperOperatorQuantumObject},
- L_p::QuantumObject{<:AbstractArray{T2},SuperOperatorQuantumObject},
- L_m::QuantumObject{<:AbstractArray{T3},SuperOperatorQuantumObject},
+function _steadystate_fourier(
+ L_0::QuantumObject{SuperOperator},
+ L_p::QuantumObject{SuperOperator},
+ L_m::QuantumObject{SuperOperator},
ωd::Number,
- solver::SSFloquetLinearSystem;
+ solver::SteadyStateLinearSolver;
n_max::Integer = 1,
tol::R = 1e-8,
kwargs...,
-) where {T1,T2,T3,R<:Real}
+) where {R<:Real}
+ T1 = eltype(L_0)
+ T2 = eltype(L_p)
+ T3 = eltype(L_m)
T = promote_type(T1, T2, T3)
L_0_mat = get_data(L_0)
@@ -341,7 +389,7 @@ function _steadystate_floquet(
N = size(L_0_mat, 1)
Ns = isqrt(N)
n_fourier = 2 * n_max + 1
- n_list = -n_max:n_max
+ n_list = (-n_max):n_max
weight = 1
Mn = sparse(ones(Ns), [Ns * (j - 1) + j for j in 1:Ns], fill(weight, Ns), N, N)
@@ -358,42 +406,53 @@ function _steadystate_floquet(
v0 = zeros(T, n_fourier * N)
v0[n_max*N+1] = weight
- Pl = ilu(M, τ = 0.01)
+ (haskey(kwargs, :Pl) || haskey(kwargs, :Pr)) && error("The use of preconditioners must be defined in the solver.")
+ if !isnothing(solver.Pl)
+ kwargs = merge((; kwargs...), (Pl = solver.Pl(M),))
+ elseif isa(M, SparseMatrixCSC)
+ kwargs = merge((; kwargs...), (Pl = ilu(M, τ = 0.01),))
+ end
+ !isnothing(solver.Pr) && (kwargs = merge((; kwargs...), (Pr = solver.Pr(M),)))
+ !haskey(kwargs, :abstol) && (kwargs = merge((; kwargs...), (abstol = tol,)))
+ !haskey(kwargs, :reltol) && (kwargs = merge((; kwargs...), (reltol = tol,)))
+
prob = LinearProblem(M, v0)
- ρtot = solve(prob, KrylovJL_GMRES(), Pl = Pl, abstol = tol, reltol = tol).u
+ ρtot = solve(prob, solver.alg; kwargs...).u
offset1 = n_max * N
offset2 = (n_max + 1) * N
- ρ0 = reshape(ρtot[offset1+1:offset2], Ns, Ns)
+ ρ0 = reshape(ρtot[(offset1+1):offset2], Ns, Ns)
ρ0_tr = tr(ρ0)
ρ0 = ρ0 / ρ0_tr
- ρ0 = QuantumObject((ρ0 + ρ0') / 2, type = Operator, dims = L_0.dims)
+ ρ0 = QuantumObject((ρ0 + ρ0') / 2, type = Operator(), dims = L_0.dimensions)
ρ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, type = Operator, dims = L_0.dims)
+ 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, type = Operator(), dims = L_0.dimensions)
push!(ρ_list, ρi_m)
end
return ρ_list
end
-function _steadystate_floquet(
- L_0::QuantumObject{<:AbstractArray,SuperOperatorQuantumObject},
- L_p::QuantumObject{<:AbstractArray,SuperOperatorQuantumObject},
- L_m::QuantumObject{<:AbstractArray,SuperOperatorQuantumObject},
+function _steadystate_fourier(
+ L_0::QuantumObject{SuperOperator},
+ L_p::QuantumObject{SuperOperator},
+ L_m::QuantumObject{SuperOperator},
ωd::Number,
solver::SSFloquetEffectiveLiouvillian;
n_max::Integer = 1,
tol::R = 1e-8,
kwargs...,
) where {R<:Real}
- ((L_0.dims == L_p.dims) && (L_0.dims == L_m.dims)) ||
- throw(DimensionMismatch("The quantum objects are not of the same Hilbert dimension."))
+ check_dimensions(L_0, L_p, L_m)
L_eff = liouvillian_floquet(L_0, L_p, L_m, ωd; n_max = n_max, tol = tol)
return steadystate(L_eff; solver = solver.steadystate_solver, kwargs...)
end
+
+# TODO: Synonym to align with QuTiP. Track https://github.com/qutip/qutip/issues/2632 when this can be removed.
+const steadystate_floquet = steadystate_fourier
diff --git a/src/time_evolution/brmesolve.jl b/src/time_evolution/brmesolve.jl
new file mode 100644
index 000000000..1ef3720aa
--- /dev/null
+++ b/src/time_evolution/brmesolve.jl
@@ -0,0 +1,226 @@
+export bloch_redfield_tensor, brterm, brmesolve
+
+@doc raw"""
+ bloch_redfield_tensor(
+ H::QuantumObject{Operator},
+ a_ops::Union{AbstractVector, Tuple},
+ c_ops::Union{AbstractVector, Tuple, Nothing}=nothing;
+ sec_cutoff::Real=0.1,
+ fock_basis::Union{Val,Bool}=Val(false)
+ )
+
+Calculates the Bloch-Redfield tensor ([`SuperOperator`](@ref)) for a system given a set of operators and corresponding spectral functions that describes the system's coupling to its environment.
+
+## Arguments
+
+- `H`: The system Hamiltonian. Must be an [`Operator`](@ref)
+- `a_ops`: Nested list with each element is a `Tuple` of operator-function pairs `(a_op, spectra)`, and the coupling [`Operator`](@ref) `a_op` must be hermitian with corresponding `spectra` being a `Function` of transition energy
+- `c_ops`: List of collapse operators corresponding to Lindblad dissipators
+- `sec_cutoff`: Cutoff for secular approximation. Use `-1` if secular approximation is not used when evaluating bath-coupling terms.
+- `fock_basis`: Whether to return the tensor in the input (fock) basis or the diagonalized (eigen) basis.
+
+## Return
+
+The return depends on `fock_basis`.
+
+- `fock_basis=Val(true)`: return the Bloch-Redfield tensor (in the fock basis) only.
+- `fock_basis=Val(false)`: return the Bloch-Redfield tensor (in the eigen basis) along with the transformation matrix from eigen to fock basis.
+"""
+function bloch_redfield_tensor(
+ H::QuantumObject{Operator},
+ a_ops::Union{AbstractVector,Tuple},
+ c_ops::Union{AbstractVector,Tuple,Nothing} = nothing;
+ sec_cutoff::Real = 0.1,
+ fock_basis::Union{Val,Bool} = Val(false),
+)
+ rst = eigenstates(H)
+ U = QuantumObject(rst.vectors, Operator(), H.dimensions)
+ sec_cutoff = float(sec_cutoff)
+
+ H_new = getVal(fock_basis) ? H : QuantumObject(Diagonal(rst.values), Operator(), H.dimensions)
+ c_ops_new = isnothing(c_ops) ? nothing : map(x -> getVal(fock_basis) ? x : U' * x * U, c_ops)
+ L0 = liouvillian(H_new, c_ops_new)
+
+ # Check whether we can rotate the terms to the eigenbasis directly in the Hamiltonian space
+ fock_basis_hamiltonian = getVal(fock_basis) && sec_cutoff == -1
+
+ R = isempty(a_ops) ? 0 : sum(x -> _brterm(rst, x[1], x[2], sec_cutoff, fock_basis_hamiltonian), a_ops)
+
+ # If in fock basis, we need to transform the terms back to the fock basis
+ # Note: we can transform the terms in the Hamiltonian space only if sec_cutoff is -1
+ # otherwise, we need to use the SU superoperator below to transform the entire Liouvillian
+ # at the end, due to the action of M_cut
+ if getVal(fock_basis)
+ if fock_basis_hamiltonian
+ return L0 + R # Already rotated in the Hamiltonian space
+ else
+ SU = sprepost(U, U')
+ return L0 + SU * R * SU'
+ end
+ else
+ return L0 + R, U
+ end
+end
+
+@doc raw"""
+ brterm(
+ H::QuantumObject{Operator},
+ a_op::QuantumObject{Operator},
+ spectra::Function;
+ sec_cutoff::Real=0.1,
+ fock_basis::Union{Bool, Val}=Val(false)
+ )
+
+Calculates the contribution of one coupling operator to the Bloch-Redfield tensor.
+
+## Argument
+
+- `H`: The system Hamiltonian. Must be an [`Operator`](@ref)
+- `a_op`: The operator coupling to the environment. Must be hermitian.
+- `spectra`: The corresponding environment spectra as a `Function` of transition energy.
+- `sec_cutoff`: Cutoff for secular approximation. Use `-1` if secular approximation is not used when evaluating bath-coupling terms.
+- `fock_basis`: Whether to return the tensor in the input (fock) basis or the diagonalized (eigen) basis.
+
+## Return
+
+The return depends on `fock_basis`.
+
+- `fock_basis=Val(true)`: return the Bloch-Redfield term (in the fock basis) only.
+- `fock_basis=Val(false)`: return the Bloch-Redfield term (in the eigen basis) along with the transformation matrix from eigen to fock basis.
+"""
+function brterm(
+ H::QuantumObject{Operator},
+ a_op::QuantumObject{Operator},
+ spectra::Function;
+ sec_cutoff::Real = 0.1,
+ fock_basis::Union{Bool,Val} = Val(false),
+)
+ rst = eigenstates(H)
+ U = QuantumObject(rst.vectors, Operator(), H.dimensions)
+
+ # Check whether we can rotate the terms to the eigenbasis directly in the Hamiltonian space
+ fock_basis_hamiltonian = getVal(fock_basis) && sec_cutoff == -1
+
+ term = _brterm(rst, a_op, spectra, sec_cutoff, fock_basis_hamiltonian)
+ if getVal(fock_basis)
+ if fock_basis_hamiltonian
+ return term # Already rotated in the Hamiltonian space
+ else
+ SU = sprepost(U, U')
+ return SU * term * SU'
+ end
+ else
+ return term, U
+ end
+end
+
+function _brterm(
+ rst::EigsolveResult,
+ a_op::T,
+ spectra::F,
+ sec_cutoff::Real,
+ fock_basis_hamiltonian::Union{Bool,Val},
+) where {T<:QuantumObject{Operator},F<:Function}
+ _check_br_spectra(spectra)
+
+ U = rst.vectors
+ Id = I(prod(rst.dimensions))
+
+ skew = @. rst.values - rst.values' |> real
+ spectrum = spectra.(skew)
+
+ A_mat = U' * a_op.data * U
+ A_mat_spec = A_mat .* spectrum
+ A_mat_spec_t = A_mat .* transpose(spectrum)
+
+ ac_term = A_mat_spec * A_mat
+ bd_term = A_mat * A_mat_spec_t
+
+ if sec_cutoff != -1
+ m_cut = similar(skew)
+ map!(x -> abs(x) < sec_cutoff, m_cut, skew)
+ ac_term .*= m_cut
+ bd_term .*= m_cut
+
+ vec_skew = vec(skew)
+ M_cut = @. abs(vec_skew - vec_skew') < sec_cutoff
+ end
+
+ # Rotate the terms to the eigenbasis if possible
+ if getVal(fock_basis_hamiltonian)
+ A_mat = U * A_mat * U'
+ A_mat_spec = U * A_mat_spec * U'
+ A_mat_spec_t = U * A_mat_spec_t * U'
+ ac_term = U * ac_term * U'
+ bd_term = U * bd_term * U'
+ end
+
+ # Remove small values before passing in the Liouville space
+ if settings.auto_tidyup
+ tidyup!(A_mat)
+ tidyup!(A_mat_spec)
+ tidyup!(A_mat_spec_t)
+ tidyup!(ac_term)
+ tidyup!(bd_term)
+ end
+
+ out = (_sprepost(A_mat_spec_t, A_mat) + _sprepost(A_mat, A_mat_spec) - _spost(ac_term, Id) - _spre(bd_term, Id)) / 2
+
+ (sec_cutoff != -1) && (out .*= M_cut)
+
+ return QuantumObject(out, SuperOperator(), rst.dimensions)
+end
+
+@doc raw"""
+ brmesolve(
+ H::QuantumObject{Operator},
+ ψ0::QuantumObject,
+ tlist::AbstractVector,
+ a_ops::Union{Nothing, AbstractVector, Tuple},
+ c_ops::Union{Nothing, AbstractVector, Tuple}=nothing;
+ sec_cutoff::Real=0.1,
+ e_ops::Union{Nothing, AbstractVector}=nothing,
+ kwargs...,
+ )
+
+Solves for the dynamics of a system using the Bloch-Redfield master equation, given an input Hamiltonian, Hermitian bath-coupling terms and their associated spectral functions, as well as possible Lindblad collapse operators.
+
+## Arguments
+
+- `H`: The system Hamiltonian. Must be an [`Operator`](@ref)
+- `ψ0`: Initial state of the system $|\psi(0)\rangle$. It can be either a [`Ket`](@ref), [`Operator`](@ref) or [`OperatorKet`](@ref).
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `a_ops`: Nested list with each element is a `Tuple` of operator-function pairs `(a_op, spectra)`, and the coupling [`Operator`](@ref) `a_op` must be hermitian with corresponding `spectra` being a `Function` of transition energy
+- `c_ops`: List of collapse operators corresponding to Lindblad dissipators
+- `sec_cutoff`: Cutoff for secular approximation. Use `-1` if secular approximation is not used when evaluating bath-coupling terms.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `kwargs`: Keyword arguments for [`mesolve`](@ref).
+
+## Notes
+
+- This function will automatically generate the [`bloch_redfield_tensor`](@ref) and solve the time evolution with [`mesolve`](@ref).
+
+# Returns
+
+- `sol::TimeEvolutionSol`: The solution of the time evolution. See also [`TimeEvolutionSol`](@ref)
+"""
+function brmesolve(
+ H::QuantumObject{Operator},
+ ψ0::QuantumObject,
+ tlist::AbstractVector,
+ a_ops::Union{Nothing,AbstractVector,Tuple},
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ sec_cutoff::Real = 0.1,
+ e_ops::Union{Nothing,AbstractVector} = nothing,
+ kwargs...,
+)
+ R = bloch_redfield_tensor(H, a_ops, c_ops; sec_cutoff = sec_cutoff, fock_basis = Val(true))
+
+ return mesolve(R, ψ0, tlist, nothing; e_ops = e_ops, kwargs...)
+end
+
+function _check_br_spectra(f::Function)
+ length(methods(f, [Real]).ms) == 0 &&
+ throw(ArgumentError("The following function must only accept one argument: `$(nameof(f))(ω)` with ω<:Real"))
+ return nothing
+end
diff --git a/src/time_evolution/callback_helpers/callback_helpers.jl b/src/time_evolution/callback_helpers/callback_helpers.jl
new file mode 100644
index 000000000..0d01fd7f6
--- /dev/null
+++ b/src/time_evolution/callback_helpers/callback_helpers.jl
@@ -0,0 +1,180 @@
+#=
+This file contains helper functions for callbacks. The affect! function are defined taking advantage of the Julia struct, which allows to store some cache exclusively for the callback.
+=#
+
+##
+
+abstract type AbstractSaveFunc end
+
+# Multiple dispatch depending on the progress_bar and e_ops types
+function _generate_se_me_kwargs(e_ops, progress_bar, tlist, kwargs, method)
+ cb = _generate_save_callback(e_ops, tlist, progress_bar, method)
+ return _merge_kwargs_with_callback(kwargs, cb)
+end
+_generate_se_me_kwargs(e_ops::Nothing, progress_bar::Val{false}, tlist, kwargs, method) = kwargs
+
+function _generate_stochastic_kwargs(
+ e_ops,
+ sc_ops,
+ progress_bar,
+ tlist,
+ store_measurement,
+ kwargs,
+ method::Type{SF},
+) where {SF<:AbstractSaveFunc}
+ cb_save = _generate_stochastic_save_callback(e_ops, sc_ops, tlist, store_measurement, progress_bar, method)
+
+ # Ensure that the noise is stored in tlist. # TODO: Fix this directly in DiffEqNoiseProcess.jl
+ # See https://github.com/SciML/DiffEqNoiseProcess.jl/issues/214 for example
+ tstops = haskey(kwargs, :tstops) ? unique!(sort!(vcat(tlist, kwargs.tstops))) : tlist
+ kwargs2 = merge(kwargs, (tstops = tstops,))
+
+ if SF === SaveFuncSSESolve
+ cb_normalize = _ssesolve_generate_normalize_cb()
+ return _merge_kwargs_with_callback(kwargs2, CallbackSet(cb_normalize, cb_save))
+ end
+
+ return _merge_kwargs_with_callback(kwargs2, cb_save)
+end
+_generate_stochastic_kwargs(
+ e_ops::Nothing,
+ sc_ops,
+ progress_bar::Val{false},
+ tlist,
+ store_measurement::Val{false},
+ kwargs,
+ method::Type{SF},
+) where {SF<:AbstractSaveFunc} = kwargs
+
+function _merge_kwargs_with_callback(kwargs, cb)
+ kwargs2 =
+ haskey(kwargs, :callback) ? merge(kwargs, (callback = CallbackSet(cb, kwargs.callback),)) :
+ merge(kwargs, (callback = cb,))
+
+ return kwargs2
+end
+
+function _generate_save_callback(e_ops, tlist, progress_bar, method)
+ e_ops_data = e_ops isa Nothing ? nothing : _get_e_ops_data(e_ops, method)
+
+ progr = getVal(progress_bar) ? ProgressBar(length(tlist), enable = getVal(progress_bar)) : nothing
+
+ expvals = e_ops isa Nothing ? nothing : Array{ComplexF64}(undef, length(e_ops), length(tlist))
+
+ _save_func = method(e_ops_data, progr, Ref(1), expvals)
+ return FunctionCallingCallback(_save_func, funcat = tlist)
+end
+
+function _generate_stochastic_save_callback(e_ops, sc_ops, tlist, store_measurement, progress_bar, method)
+ e_ops_data = e_ops isa Nothing ? nothing : _get_e_ops_data(e_ops, method)
+ m_ops_data = _get_m_ops_data(sc_ops, method)
+
+ progr = getVal(progress_bar) ? ProgressBar(length(tlist), enable = getVal(progress_bar)) : nothing
+
+ expvals = e_ops isa Nothing ? nothing : Array{ComplexF64}(undef, length(e_ops), length(tlist))
+ m_expvals = getVal(store_measurement) ? Array{Float64}(undef, length(sc_ops), length(tlist) - 1) : nothing
+
+ _save_func_cache = Array{Float64}(undef, length(sc_ops))
+ _save_func =
+ method(store_measurement, e_ops_data, m_ops_data, progr, Ref(1), expvals, m_expvals, tlist, _save_func_cache)
+ return FunctionCallingCallback(_save_func, funcat = tlist)
+end
+
+##
+
+# When e_ops is Nothing. Common for all solvers
+function _save_func(integrator, progr)
+ next!(progr)
+ u_modified!(integrator, false)
+ return nothing
+end
+
+# When progr is Nothing. Common for all solvers
+function _save_func(integrator, progr::Nothing)
+ u_modified!(integrator, false)
+ return nothing
+end
+
+##
+
+#=
+ To extract the measurement outcomes of a stochastic solver
+=#
+function _get_m_expvals(integrator::AbstractODESolution, method::Type{SF}) where {SF<:AbstractSaveFunc}
+ cb = _get_save_callback(integrator, method)
+ if cb isa Nothing
+ return nothing
+ else
+ return cb.affect!.func.m_expvals
+ end
+end
+
+#=
+ With this function we extract the e_ops from the SaveFuncMCSolve `affect!` function of the callback of the integrator.
+ This callback can only be a FunctionCallingCallback (DiscreteCallback).
+=#
+function _get_e_ops(integrator::AbstractODEIntegrator, method::Type{SF}) where {SF<:AbstractSaveFunc}
+ cb = _get_save_callback(integrator, method)
+ if cb isa Nothing
+ return nothing
+ else
+ return cb.affect!.func.e_ops
+ end
+end
+
+# Get the e_ops from a given AbstractODESolution. Valid for `sesolve`, `mesolve` and `ssesolve`.
+function _get_expvals(sol::AbstractODESolution, method::Type{SF}) where {SF<:AbstractSaveFunc}
+ cb = _get_save_callback(sol, method)
+ if cb isa Nothing
+ return nothing
+ else
+ return cb.affect!.func.expvals
+ end
+end
+
+#=
+ _get_save_callback
+
+Return the Callback that is responsible for saving the expectation values of the system.
+=#
+function _get_save_callback(sol::AbstractODESolution, method::Type{SF}) where {SF<:AbstractSaveFunc}
+ kwargs = NamedTuple(sol.prob.kwargs) # Convert to NamedTuple to support Zygote.jl
+ if hasproperty(kwargs, :callback)
+ return _get_save_callback(kwargs.callback, method)
+ else
+ return nothing
+ end
+end
+_get_save_callback(integrator::AbstractODEIntegrator, method::Type{SF}) where {SF<:AbstractSaveFunc} =
+ _get_save_callback(integrator.opts.callback, method)
+function _get_save_callback(cb::CallbackSet, method::Type{SF}) where {SF<:AbstractSaveFunc}
+ cbs_discrete = cb.discrete_callbacks
+ if length(cbs_discrete) > 0
+ idx = _get_save_callback_idx(cb, method)
+ _cb = cb.discrete_callbacks[idx]
+ return _get_save_callback(_cb, method)
+ else
+ return nothing
+ end
+end
+function _get_save_callback(cb::DiscreteCallback, ::Type{SF}) where {SF<:AbstractSaveFunc}
+ if typeof(cb.affect!) <: FunctionCallingAffect && typeof(cb.affect!.func) <: AbstractSaveFunc
+ return cb
+ end
+ return nothing
+end
+_get_save_callback(cb::ContinuousCallback, ::Type{SF}) where {SF<:AbstractSaveFunc} = nothing
+
+_get_save_callback_idx(cb, method) = 1
+
+# %% ------------ Noise Measurement Helpers ------------ %%
+
+# TODO: To improve. See https://github.com/SciML/DiffEqNoiseProcess.jl/issues/214
+function _homodyne_dWdt!(dWdt_cache, integrator, tlist, iter)
+ idx = findfirst(>=(tlist[iter[]-1]), integrator.W.t)
+
+ # We are assuming that the last element is tlist[iter[]]
+ @inbounds dWdt_cache .= (integrator.W.u[end] .- integrator.W.u[idx]) ./ (integrator.W.t[end] - integrator.W.t[idx])
+
+ return nothing
+end
diff --git a/src/time_evolution/callback_helpers/mcsolve_callback_helpers.jl b/src/time_evolution/callback_helpers/mcsolve_callback_helpers.jl
new file mode 100644
index 000000000..d96b50202
--- /dev/null
+++ b/src/time_evolution/callback_helpers/mcsolve_callback_helpers.jl
@@ -0,0 +1,290 @@
+#=
+Helper functions for the mcsolve callbacks.
+=#
+
+struct SaveFuncMCSolve{TE,IT,TEXPV} <: AbstractSaveFunc
+ e_ops::TE
+ iter::IT
+ expvals::TEXPV
+end
+
+(f::SaveFuncMCSolve)(u, t, integrator) = _save_func_mcsolve(u, integrator, f.e_ops, f.iter, f.expvals)
+
+_get_save_callback_idx(cb, ::Type{SaveFuncMCSolve}) = _mcsolve_has_continuous_jump(cb) ? 1 : 2
+
+##
+struct LindbladJump{
+ T1,
+ T2,
+ RNGType<:AbstractRNG,
+ RandT,
+ CT<:AbstractVector,
+ WT<:AbstractVector,
+ JTT<:AbstractVector,
+ JWT<:AbstractVector,
+ JTWIT,
+}
+ c_ops::T1
+ c_ops_herm::T2
+ traj_rng::RNGType
+ random_n::RandT
+ cache_mc::CT
+ weights_mc::WT
+ cumsum_weights_mc::WT
+ col_times::JTT
+ col_which::JWT
+ col_times_which_idx::JTWIT
+end
+
+(f::LindbladJump)(integrator) = _lindblad_jump_affect!(
+ integrator,
+ f.c_ops,
+ f.c_ops_herm,
+ f.traj_rng,
+ f.random_n,
+ f.cache_mc,
+ f.weights_mc,
+ f.cumsum_weights_mc,
+ f.col_times,
+ f.col_which,
+ f.col_times_which_idx,
+)
+
+##
+
+function _save_func_mcsolve(u, integrator, e_ops, iter, expvals)
+ cache_mc = _mc_get_jump_callback(integrator).affect!.cache_mc
+
+ copyto!(cache_mc, u)
+ normalize!(cache_mc)
+ ψ = cache_mc
+ _expect = op -> dot(ψ, op, ψ)
+ @. expvals[:, iter[]] = _expect(e_ops)
+ iter[] += 1
+
+ u_modified!(integrator, false)
+ return nothing
+end
+
+function _generate_mcsolve_kwargs(ψ0, T, e_ops, tlist, c_ops, jump_callback, rng, kwargs)
+ c_ops_data = get_data.(c_ops)
+ c_ops_herm_data = map(op -> op' * op, c_ops_data)
+
+ cache_mc = similar(ψ0.data, T)
+ weights_mc = Vector{Float64}(undef, length(c_ops))
+ cumsum_weights_mc = similar(weights_mc)
+
+ col_times = Vector{Float64}(undef, COL_TIMES_WHICH_INIT_SIZE)
+ col_which = Vector{Int}(undef, COL_TIMES_WHICH_INIT_SIZE)
+ col_times_which_idx = Ref(1)
+
+ random_n = Ref(rand(rng))
+
+ _affect! = LindbladJump(
+ c_ops_data,
+ c_ops_herm_data,
+ rng,
+ random_n,
+ cache_mc,
+ weights_mc,
+ cumsum_weights_mc,
+ col_times,
+ col_which,
+ col_times_which_idx,
+ )
+
+ if jump_callback isa DiscreteLindbladJumpCallback
+ cb1 = DiscreteCallback(_mcsolve_discrete_condition, _affect!, save_positions = (false, false))
+ else
+ cb1 = ContinuousCallback(
+ _mcsolve_continuous_condition,
+ _affect!,
+ nothing,
+ interp_points = jump_callback.interp_points,
+ save_positions = (false, false),
+ )
+ end
+
+ if e_ops isa Nothing
+ # We are implicitly saying that we don't have a `ProgressBar`
+ kwargs2 =
+ haskey(kwargs, :callback) ? merge(kwargs, (callback = CallbackSet(cb1, kwargs.callback),)) :
+ merge(kwargs, (callback = cb1,))
+ return kwargs2
+ else
+ expvals = Array{ComplexF64}(undef, length(e_ops), length(tlist))
+
+ _save_func = SaveFuncMCSolve(get_data.(e_ops), Ref(1), expvals)
+ cb2 = FunctionCallingCallback(_save_func, funcat = tlist)
+ kwargs2 =
+ haskey(kwargs, :callback) ? merge(kwargs, (callback = CallbackSet(cb1, cb2, kwargs.callback),)) :
+ merge(kwargs, (callback = CallbackSet(cb1, cb2),))
+ return kwargs2
+ end
+end
+
+function _lindblad_jump_affect!(
+ integrator,
+ c_ops,
+ c_ops_herm,
+ traj_rng,
+ random_n,
+ cache_mc,
+ weights_mc,
+ cumsum_weights_mc,
+ col_times,
+ col_which,
+ col_times_which_idx,
+)
+ ψ = integrator.u
+
+ @inbounds for i in eachindex(weights_mc)
+ weights_mc[i] = real(dot(ψ, c_ops_herm[i], ψ))
+ end
+ cumsum!(cumsum_weights_mc, weights_mc)
+ r = rand(traj_rng) * sum(weights_mc)
+ collapse_idx = getindex(1:length(weights_mc), findfirst(>(r), cumsum_weights_mc))
+ mul!(cache_mc, c_ops[collapse_idx], ψ)
+ normalize!(cache_mc)
+ copyto!(integrator.u, cache_mc)
+
+ random_n[] = rand(traj_rng)
+
+ idx = col_times_which_idx[]
+ @inbounds col_times[idx] = integrator.t
+ @inbounds col_which[idx] = collapse_idx
+ col_times_which_idx[] += 1
+ if col_times_which_idx[] > length(col_times)
+ resize!(col_times, length(col_times) + COL_TIMES_WHICH_INIT_SIZE)
+ resize!(col_which, length(col_which) + COL_TIMES_WHICH_INIT_SIZE)
+ end
+ u_modified!(integrator, true)
+ return nothing
+end
+
+_mcsolve_continuous_condition(u, t, integrator) =
+ @inbounds _mc_get_jump_callback(integrator).affect!.random_n[] - real(dot(u, u))
+
+_mcsolve_discrete_condition(u, t, integrator) =
+ @inbounds real(dot(u, u)) < _mc_get_jump_callback(integrator).affect!.random_n[]
+
+##
+
+function _mc_get_jump_callback(sol::AbstractODESolution)
+ kwargs = NamedTuple(sol.prob.kwargs) # Convert to NamedTuple to support Zygote.jl
+ return _mc_get_jump_callback(kwargs.callback) # There is always the Jump callback
+end
+_mc_get_jump_callback(integrator::AbstractODEIntegrator) = _mc_get_jump_callback(integrator.opts.callback)
+_mc_get_jump_callback(cb::CallbackSet) =
+ if _mcsolve_has_continuous_jump(cb)
+ return cb.continuous_callbacks[1]
+ else
+ return cb.discrete_callbacks[1]
+ end
+_mc_get_jump_callback(cb::ContinuousCallback) = cb
+_mc_get_jump_callback(cb::DiscreteCallback) = cb
+
+##
+
+#=
+ With this function we extract the c_ops and c_ops_herm from the LindbladJump `affect!` function of the callback of the integrator.
+ This callback can be a DiscreteLindbladJumpCallback or a ContinuousLindbladJumpCallback.
+=#
+function _mcsolve_get_c_ops(integrator::AbstractODEIntegrator)
+ cb = _mc_get_jump_callback(integrator)
+ if cb isa Nothing
+ return nothing
+ else
+ return cb.affect!.c_ops, cb.affect!.c_ops_herm
+ end
+end
+
+#=
+ _mcsolve_initialize_callbacks(prob, tlist)
+
+Return the same callbacks of the `prob`, but with the `iter` variable reinitialized to 1 and the `expvals` variable reinitialized to a new matrix.
+=#
+function _mcsolve_initialize_callbacks(prob, tlist, traj_rng)
+ cb = prob.kwargs[:callback]
+ return _mcsolve_initialize_callbacks(cb, tlist, traj_rng)
+end
+function _mcsolve_initialize_callbacks(cb::CallbackSet, tlist, traj_rng)
+ cb_continuous = cb.continuous_callbacks
+ cb_discrete = cb.discrete_callbacks
+
+ if _mcsolve_has_continuous_jump(cb)
+ idx = 1
+ if cb_discrete[idx].affect!.func isa SaveFuncMCSolve
+ e_ops = cb_discrete[idx].affect!.func.e_ops
+ expvals = similar(cb_discrete[idx].affect!.func.expvals)
+ _save_func = SaveFuncMCSolve(e_ops, Ref(1), expvals)
+ cb_save = (FunctionCallingCallback(_save_func, funcat = tlist),)
+ else
+ cb_save = ()
+ end
+
+ _jump_affect! = _similar_affect!(cb_continuous[1].affect!, traj_rng)
+ cb_jump = _modify_field(cb_continuous[1], :affect!, _jump_affect!)
+
+ return CallbackSet((cb_jump, cb_continuous[2:end]...), (cb_save..., cb_discrete[2:end]...))
+ else
+ idx = 2
+ if cb_discrete[idx].affect!.func isa SaveFuncMCSolve
+ e_ops = cb_discrete[idx].affect!.func.e_ops
+ expvals = similar(cb_discrete[idx].affect!.func.expvals)
+ _save_func = SaveFuncMCSolve(e_ops, Ref(1), expvals)
+ cb_save = (FunctionCallingCallback(_save_func, funcat = tlist),)
+ else
+ cb_save = ()
+ end
+
+ _jump_affect! = _similar_affect!(cb_discrete[1].affect!, traj_rng)
+ cb_jump = _modify_field(cb_discrete[1], :affect!, _jump_affect!)
+
+ return CallbackSet(cb_continuous, (cb_jump, cb_save..., cb_discrete[3:end]...))
+ end
+end
+function _mcsolve_initialize_callbacks(cb::CBT, tlist, traj_rng) where {CBT<:Union{ContinuousCallback,DiscreteCallback}}
+ _jump_affect! = _similar_affect!(cb.affect!, traj_rng)
+ return _modify_field(cb, :affect!, _jump_affect!)
+end
+
+#=
+ _similar_affect!
+
+Return a new LindbladJump with the same fields as the input LindbladJump but with new memory.
+=#
+function _similar_affect!(affect::LindbladJump, traj_rng)
+ random_n = Ref(rand(traj_rng))
+ cache_mc = similar(affect.cache_mc)
+ weights_mc = similar(affect.weights_mc)
+ cumsum_weights_mc = similar(affect.cumsum_weights_mc)
+ col_times = similar(affect.col_times)
+ col_which = similar(affect.col_which)
+ col_times_which_idx = Ref(1)
+
+ return LindbladJump(
+ affect.c_ops,
+ affect.c_ops_herm,
+ traj_rng,
+ random_n,
+ cache_mc,
+ weights_mc,
+ cumsum_weights_mc,
+ col_times,
+ col_which,
+ col_times_which_idx,
+ )
+end
+
+Base.@constprop :aggressive function _modify_field(obj::T, field_name::Symbol, field_val) where {T}
+ # Create a NamedTuple of fields, deepcopying only the selected ones
+ fields = (name != field_name ? (getfield(obj, name)) : field_val for name in fieldnames(T))
+ # Reconstruct the struct with the updated fields
+ return Base.typename(T).wrapper(fields...)
+end
+
+_mcsolve_has_continuous_jump(cb::CallbackSet) =
+ (length(cb.continuous_callbacks) > 0) && (cb.continuous_callbacks[1].affect! isa LindbladJump)
+_mcsolve_has_continuous_jump(cb::ContinuousCallback) = true
+_mcsolve_has_continuous_jump(cb::DiscreteCallback) = false
diff --git a/src/time_evolution/callback_helpers/mesolve_callback_helpers.jl b/src/time_evolution/callback_helpers/mesolve_callback_helpers.jl
new file mode 100644
index 000000000..a24229d29
--- /dev/null
+++ b/src/time_evolution/callback_helpers/mesolve_callback_helpers.jl
@@ -0,0 +1,43 @@
+#=
+Helper functions for the mesolve callbacks.
+=#
+
+struct SaveFuncMESolve{TE,PT<:Union{Nothing,ProgressBar},IT,TEXPV<:Union{Nothing,AbstractMatrix}} <: AbstractSaveFunc
+ e_ops::TE
+ progr::PT
+ iter::IT
+ expvals::TEXPV
+end
+
+(f::SaveFuncMESolve)(u, t, integrator) = _save_func_mesolve(u, integrator, f.e_ops, f.progr, f.iter, f.expvals)
+(f::SaveFuncMESolve{Nothing})(u, t, integrator) = _save_func(integrator, f.progr)
+
+_get_e_ops_data(e_ops, ::Type{SaveFuncMESolve}) = [_generate_mesolve_e_op(op) for op in e_ops] # Broadcasting generates type instabilities on Julia v1.10
+
+##
+
+# When e_ops is a list of operators
+function _save_func_mesolve(u, integrator, e_ops, progr, iter, expvals)
+ # This is equivalent to tr(op * ρ), when both are matrices.
+ # The advantage of using this convention is that We don't need
+ # to reshape u to make it a matrix, but we reshape the e_ops once.
+
+ ρ = u
+ _expect = op -> dot(op, ρ)
+ @. expvals[:, iter[]] = _expect(e_ops)
+ iter[] += 1
+
+ return _save_func(integrator, progr)
+end
+
+function _mesolve_callbacks_new_e_ops!(integrator::AbstractODEIntegrator, e_ops)
+ cb = _get_save_callback(integrator, SaveFuncMESolve)
+ if cb isa Nothing
+ return nothing
+ else
+ cb.affect!.func.e_ops .= e_ops # Only works if e_ops is a Vector of operators
+ return nothing
+ end
+end
+
+_generate_mesolve_e_op(op) = mat2vec(adjoint(get_data(op)))
diff --git a/src/time_evolution/callback_helpers/sesolve_callback_helpers.jl b/src/time_evolution/callback_helpers/sesolve_callback_helpers.jl
new file mode 100644
index 000000000..2bbff8bf0
--- /dev/null
+++ b/src/time_evolution/callback_helpers/sesolve_callback_helpers.jl
@@ -0,0 +1,27 @@
+#=
+Helper functions for the sesolve callbacks.
+=#
+
+struct SaveFuncSESolve{TE,PT<:Union{Nothing,ProgressBar},IT,TEXPV<:Union{Nothing,AbstractMatrix}} <: AbstractSaveFunc
+ e_ops::TE
+ progr::PT
+ iter::IT
+ expvals::TEXPV
+end
+
+(f::SaveFuncSESolve)(u, t, integrator) = _save_func_sesolve(u, integrator, f.e_ops, f.progr, f.iter, f.expvals)
+(f::SaveFuncSESolve{Nothing})(u, t, integrator) = _save_func(integrator, f.progr) # Common for both mesolve and sesolve
+
+_get_e_ops_data(e_ops, ::Type{SaveFuncSESolve}) = get_data.(e_ops)
+
+##
+
+# When e_ops is a list of operators
+function _save_func_sesolve(u, integrator, e_ops, progr, iter, expvals)
+ ψ = u
+ _expect = op -> dot(ψ, op, ψ)
+ @. expvals[:, iter[]] = _expect(e_ops)
+ iter[] += 1
+
+ return _save_func(integrator, progr)
+end
diff --git a/src/time_evolution/callback_helpers/smesolve_callback_helpers.jl b/src/time_evolution/callback_helpers/smesolve_callback_helpers.jl
new file mode 100644
index 000000000..6fb75a979
--- /dev/null
+++ b/src/time_evolution/callback_helpers/smesolve_callback_helpers.jl
@@ -0,0 +1,59 @@
+#=
+Helper functions for the smesolve callbacks. Almost equal to the mesolve case, but with an additional possibility to store the measurement operators expectation values.
+=#
+
+struct SaveFuncSMESolve{
+ SM,
+ TE,
+ TE2,
+ PT<:Union{Nothing,ProgressBar},
+ IT,
+ TEXPV<:Union{Nothing,AbstractMatrix},
+ TMEXPV<:Union{Nothing,AbstractMatrix},
+ TLT<:AbstractVector,
+ CT<:AbstractVector,
+} <: AbstractSaveFunc
+ store_measurement::Val{SM}
+ e_ops::TE
+ m_ops::TE2
+ progr::PT
+ iter::IT
+ expvals::TEXPV
+ m_expvals::TMEXPV
+ tlist::TLT
+ dWdt_cache::CT
+end
+
+(f::SaveFuncSMESolve)(u, t, integrator) =
+ _save_func_smesolve(u, integrator, f.e_ops, f.m_ops, f.progr, f.iter, f.expvals, f.m_expvals, f.tlist, f.dWdt_cache)
+(f::SaveFuncSMESolve{false,Nothing})(u, t, integrator) = _save_func(integrator, f.progr) # Common for both all solvers
+
+_get_e_ops_data(e_ops, ::Type{SaveFuncSMESolve}) = _get_e_ops_data(e_ops, SaveFuncMESolve)
+_get_m_ops_data(sc_ops, ::Type{SaveFuncSMESolve}) =
+ map(op -> _generate_mesolve_e_op(op) + _generate_mesolve_e_op(op'), sc_ops)
+
+##
+
+# When e_ops is a list of operators
+function _save_func_smesolve(u, integrator, e_ops, m_ops, progr, iter, expvals, m_expvals, tlist, dWdt_cache)
+ # This is equivalent to tr(op * ρ), when both are matrices.
+ # The advantage of using this convention is that We don't need
+ # to reshape u to make it a matrix, but we reshape the e_ops once.
+
+ ρ = u
+
+ _expect = op -> dot(op, ρ)
+
+ if !isnothing(e_ops)
+ @. expvals[:, iter[]] = _expect(e_ops)
+ end
+
+ if !isnothing(m_expvals) && iter[] > 1
+ _homodyne_dWdt!(dWdt_cache, integrator, tlist, iter)
+ @. m_expvals[:, iter[]-1] = real(_expect(m_ops)) + dWdt_cache
+ end
+
+ iter[] += 1
+
+ return _save_func(integrator, progr)
+end
diff --git a/src/time_evolution/callback_helpers/ssesolve_callback_helpers.jl b/src/time_evolution/callback_helpers/ssesolve_callback_helpers.jl
new file mode 100644
index 000000000..e28ccc6ef
--- /dev/null
+++ b/src/time_evolution/callback_helpers/ssesolve_callback_helpers.jl
@@ -0,0 +1,69 @@
+#=
+Helper functions for the ssesolve callbacks. Almost equal to the sesolve case, but with an additional possibility to store the measurement operators expectation values. Also, this callback is not the first one, but the second one, due to the presence of the normalization callback.
+=#
+
+struct SaveFuncSSESolve{
+ SM,
+ TE,
+ TE2,
+ PT<:Union{Nothing,ProgressBar},
+ IT,
+ TEXPV<:Union{Nothing,AbstractMatrix},
+ TMEXPV<:Union{Nothing,AbstractMatrix},
+ TLT<:AbstractVector,
+ CT<:AbstractVector,
+} <: AbstractSaveFunc
+ store_measurement::Val{SM}
+ e_ops::TE
+ m_ops::TE2
+ progr::PT
+ iter::IT
+ expvals::TEXPV
+ m_expvals::TMEXPV
+ tlist::TLT
+ dWdt_cache::CT
+end
+
+(f::SaveFuncSSESolve)(u, t, integrator) =
+ _save_func_ssesolve(u, integrator, f.e_ops, f.m_ops, f.progr, f.iter, f.expvals, f.m_expvals, f.tlist, f.dWdt_cache)
+(f::SaveFuncSSESolve{false,Nothing})(u, t, integrator) = _save_func(integrator, f.progr) # Common for both all solvers
+
+_get_e_ops_data(e_ops, ::Type{SaveFuncSSESolve}) = get_data.(e_ops)
+_get_m_ops_data(sc_ops, ::Type{SaveFuncSSESolve}) = map(op -> Hermitian(get_data(op) + get_data(op)'), sc_ops)
+
+_get_save_callback_idx(cb, ::Type{SaveFuncSSESolve}) = 2 # The first one is the normalization callback
+
+##
+
+# When e_ops is a list of operators
+function _save_func_ssesolve(u, integrator, e_ops, m_ops, progr, iter, expvals, m_expvals, tlist, dWdt_cache)
+ ψ = u
+
+ _expect = op -> dot(ψ, op, ψ)
+
+ if !isnothing(e_ops)
+ @. expvals[:, iter[]] = _expect(e_ops)
+ end
+
+ if !isnothing(m_expvals) && iter[] > 1
+ _homodyne_dWdt!(dWdt_cache, integrator, tlist, iter)
+ @. m_expvals[:, iter[]-1] = real(_expect(m_ops)) + dWdt_cache
+ end
+
+ iter[] += 1
+
+ return _save_func(integrator, progr)
+end
+
+##
+
+#=
+ This function generates the normalization callback. It is needed to stabilize the integration when using the ssesolve method.
+=#
+function _ssesolve_generate_normalize_cb()
+ _condition = (u, t, integrator) -> true
+ _affect! = (integrator) -> normalize!(integrator.u)
+ cb = DiscreteCallback(_condition, _affect!; save_positions = (false, false))
+
+ return cb
+end
diff --git a/src/time_evolution/lr_mesolve.jl b/src/time_evolution/lr_mesolve.jl
index eeda3efa7..de49df963 100644
--- a/src/time_evolution/lr_mesolve.jl
+++ b/src/time_evolution/lr_mesolve.jl
@@ -1,82 +1,85 @@
-export lr_mesolve, lr_mesolveProblem, LRTimeEvolutionSol, LRMesolveOptions
-
-#=======================================================#
-# STRUCT DEFINITIONS
-#=======================================================#
-
-struct LRTimeEvolutionSol{TT<:Vector{<:Real},TS<:AbstractVector,TE<:Matrix{ComplexF64},TM<:Vector{<:Integer}}
- times::TT
- z::TS
- B::TS
- expvals::TE
- funvals::TE
- M::TM
-end
+export lr_mesolve, lr_mesolveProblem, TimeEvolutionLRSol
-struct LRMesolveOptions{AlgType<:OrdinaryDiffEqAlgorithm}
- alg::AlgType
- progress::Bool
- err_max::Real
- p0::Real
- atol_inv::Real
- M_max::Integer
- compute_Si::Bool
- is_dynamical::Bool
- adj_condition::String
- Δt::Real
+@doc raw"""
+ struct TimeEvolutionLRSol
+
+A structure storing the results and some information from solving low-rank master equation time evolution.
+
+# Fields (Attributes)
+
+- `times::AbstractVector`: The list of time points at which the expectation values are calculated during the evolution.
+- `times_states::AbstractVector`: The list of time points at which the states are stored during the evolution.
+- `states::Vector{QuantumObject}`: The list of result states corresponding to each time point in `times_states`.
+- `expect::Matrix`: The expectation values corresponding to each time point in `times`.
+- `fexpect::Matrix`: The function values corresponding to each time point in `times`.
+- `retcode`: The return code from the solver.
+- `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.
+- `z::Vector{QuantumObject}`: The `z`` matrix of the low-rank algorithm at each time point.
+- `B::Vector{QuantumObject}`: The `B` matrix of the low-rank algorithm at each time point.
+"""
+struct TimeEvolutionLRSol{
+ TT1<:AbstractVector{<:Real},
+ TT2<:AbstractVector{<:Real},
+ TS<:AbstractVector,
+ TE<:Matrix{ComplexF64},
+ RetT<:Enum,
+ AlgT<:OrdinaryDiffEqAlgorithm,
+ AT<:Real,
+ RT<:Real,
+ TSZB<:AbstractVector,
+ TM<:Vector{<:Integer},
+}
+ times::TT1
+ times_states::TT2
+ states::TS
+ expect::TE
+ fexpect::TE
+ retcode::RetT
+ alg::AlgT
+ abstol::AT
+ reltol::RT
+ z::TSZB
+ B::TSZB
+ M::TM
end
-function LRMesolveOptions(;
- alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- progress::Bool = true,
- err_max::Real = 0.0,
- p0::Real = 0.0,
- atol_inv::Real = 1e-4,
- M_max::Integer = typemax(Int),
- compute_Si::Bool = true,
- _is_dynamical::Bool = err_max > 0,
- adj_condition::String = "variational",
- Δt::Real = 0.0,
+lr_mesolve_options_default = (
+ alg = Tsit5(),
+ progress = true,
+ err_max = 0.0,
+ p0 = 0.0,
+ atol_inv = 1e-4,
+ M_max = typemax(Int),
+ compute_Si = true,
+ is_dynamical = false,
+ adj_condition = "variational",
+ Δt = 0.0,
)
- return LRMesolveOptions{typeof(alg)}(
- alg,
- progress,
- err_max,
- p0,
- atol_inv,
- M_max,
- compute_Si,
- _is_dynamical,
- adj_condition,
- Δt,
- )
-end
-#=======================================================#
-# ADDITIONAL FUNCTIONS
-#=======================================================#
+#=
+ ADDITIONAL FUNCTIONS
+=#
select(x::Real, xarr::AbstractArray, retval = false) = retval ? xarr[argmin(abs.(x .- xarr))] : argmin(abs.(x .- xarr))
-@doc raw"""
- _pinv!(A, T1, T2; atol::Real=0.0, rtol::Real=(eps(real(float(oneunit(T))))*min(size(A)...))*iszero(atol)) where T
- Computes the pseudo-inverse of a matrix A, and stores it in T1. If T2 is provided, it is used as a temporary matrix.
- The algorithm is based on the SVD decomposition of A, and is taken from the Julia package LinearAlgebra.
- The difference with respect to the original function is that the cutoff is done with a smooth function instead of a step function.
-
- Parameters
- ----------
- A : AbstractMatrix{T}
- The matrix to be inverted.
- T1 : AbstractMatrix{T}
- T2 : AbstractMatrix{T}
- Temporary matrices used in the calculation.
- atol : Real
- Absolute tolerance for the calculation of the pseudo-inverse.
- rtol : Real
- Relative tolerance for the calculation of the pseudo-inverse.
-"""
-function _pinv!(
+#=
+ _pinv_smooth!(A, T1, T2; atol::Real=0.0, rtol::Real=(eps(real(float(oneunit(T))))*min(size(A)...))*iszero(atol)) where T
+
+Computes the pseudo-inverse of a matrix A, and stores it in T1. If T2 is provided, it is used as a temporary matrix.
+The algorithm is based on the SVD decomposition of A, and is taken from the Julia package LinearAlgebra.
+The difference with respect to the original function is that the cutoff is done with a smooth function instead of a step function.
+
+# Arguments
+
+ - `A::AbstractMatrix`: The matrix to be inverted.
+ - `T1::AbstractMatrix`: The matrix where the pseudo-inverse is stored.
+ - `T2::AbstractMatrix`: A temporary matrix.
+ - `atol::Real`: The absolute tolerance.
+ - `rtol::Real`: The relative tolerance.
+=#
+function _pinv_smooth!(
A::AbstractMatrix{T},
T1::AbstractMatrix{T},
T2::AbstractMatrix{T};
@@ -98,22 +101,19 @@ function _pinv!(
return mul!(T1, SVD.Vt', T2)
end
-@doc raw"""
- _calculate_expectation!(p,z,B,idx) where T
- Calculates the expectation values and function values of the operators and functions in p.e_ops and p.f_ops, respectively, and stores them in p.expvals and p.funvals.
- The function is called by the callback _save_affect_lr_mesolve!.
-
- Parameters
- ----------
- p : NamedTuple
- The parameters of the problem.
- z : AbstractMatrix{T}
- The z matrix.
- B : AbstractMatrix{T}
- The B matrix.
- idx : Integer
- The index of the current time step.
-"""
+#=
+ _calculate_expectation!(p,z,B,idx)
+
+Calculates the expectation values and function values of the operators and functions in p.e_ops and p.f_ops, respectively, and stores them in p.expvals and p.funvals.
+The function is called by the callback _save_affect_lr_mesolve!.
+
+# Arguments
+
+ - `p::NamedTuple`: The parameters of the problem.
+ - `z::AbstractMatrix`: The z matrix of the low-rank algorithm.
+ - `B::AbstractMatrix`: The B matrix of the low-rank algorithm.
+ - `idx::Integer`: The index of the current time step.
+=#
function _calculate_expectation!(p, z, B, idx)
e_ops = p.e_ops
f_ops = p.f_ops
@@ -136,9 +136,9 @@ function _calculate_expectation!(p, z, B, idx)
end
end
-#=======================================================#
-# SAVING FUNCTIONS
-#=======================================================#
+#=
+ SAVING FUNCTIONS
+=#
function _periodicsave_func(integrator)
ip = integrator.p
@@ -147,42 +147,40 @@ function _periodicsave_func(integrator)
return u_modified!(integrator, false)
end
-_save_control_lr_mesolve(u, t, integrator) = t in integrator.p.t_l
+_save_control_lr_mesolve(u, t, integrator) = t in integrator.p.times
function _save_affect_lr_mesolve!(integrator)
ip = integrator.p
N, M = ip.N, ip.M
- idx = select(integrator.t, ip.t_l)
+ idx = select(integrator.t, ip.times)
- @views z = reshape(integrator.u[1:N*M], N, M)
- @views B = reshape(integrator.u[N*M+1:end], M, M)
+ @views z = reshape(integrator.u[1:(N*M)], N, M)
+ @views B = reshape(integrator.u[(N*M+1):end], M, M)
_calculate_expectation!(ip, z, B, idx)
if integrator.p.opt.progress
- print("\rProgress: $(round(Int, 100*idx/length(ip.t_l)))%")
+ print("\rProgress: $(round(Int, 100*idx/length(ip.times)))%")
flush(stdout)
end
return u_modified!(integrator, false)
end
-#=======================================================#
-# CALLBACK FUNCTIONS
-#=======================================================#
+#=
+ CALLBACK FUNCTIONS
+=#
-@doc raw"""
- _adjM_condition_ratio(u, t, integrator) where T
- Condition for the dynamical rank adjustment based on the ratio between the smallest and largest eigenvalues of the density matrix.
- The spectrum of the density matrix is calculated efficiently using the properties of the SVD decomposition of the matrix.
-
- Parameters
- ----------
- u : AbstractVector{T}
- The current state of the system.
- t : Real
- The current time.
- integrator : ODEIntegrator
- The integrator of the problem.
-"""
+#=
+ _adjM_condition_ratio(u, t, integrator)
+
+Condition for the dynamical rank adjustment based on the ratio between the smallest and largest eigenvalues of the density matrix.
+The spectrum of the density matrix is calculated efficiently using the properties of the SVD decomposition of the matrix.
+
+# Arguments
+
+ - `u::AbstractVector`: The current state of the system.
+ - `t::Real`: The current time.
+ - `integrator::ODEIntegrator`: The integrator of the problem.
+=#
function _adjM_condition_ratio(u, t, integrator)
ip = integrator.p
opt = ip.opt
@@ -190,8 +188,8 @@ function _adjM_condition_ratio(u, t, integrator)
C = ip.A0
σ = ip.temp_MM
- @views z = reshape(u[1:N*M], N, M)
- @views B = reshape(u[N*M+1:end], M, M)
+ @views z = reshape(u[1:(N*M)], N, M)
+ @views B = reshape(u[(N*M+1):end], M, M)
mul!(C, z, sqrt(B))
mul!(σ, C', C)
p = abs.(eigvals(σ))
@@ -200,19 +198,17 @@ function _adjM_condition_ratio(u, t, integrator)
return (err >= opt.err_max && M < N && M < opt.M_max)
end
-@doc raw"""
- _adjM_condition_variational(u, t, integrator) where T
- Condition for the dynamical rank adjustment based on the leakage out of the low-rank manifold.
-
- Parameters
- ----------
- u : AbstractVector{T}
- The current state of the system.
- t : Real
- The current time.
- integrator : ODEIntegrator
- The integrator of the problem.
-"""
+#=
+ _adjM_condition_variational(u, t, integrator)
+
+Condition for the dynamical rank adjustment based on the leakage out of the low-rank manifold.
+
+# Arguments
+
+ - `u::AbstractVector`: The current state of the system.
+ - `t::Real`: The current time.
+ - `integrator::ODEIntegrator`: The integrator of the problem.
+=#
function _adjM_condition_variational(u, t, integrator)
ip = integrator.p
opt = ip.opt
@@ -222,16 +218,16 @@ function _adjM_condition_variational(u, t, integrator)
return (err >= opt.err_max && M < N && M < opt.M_max)
end
-@doc raw"""
+#=
_adjM_affect!(integrator)
- Affect function for the dynamical rank adjustment. It increases the rank of the low-rank manifold by one, and updates the matrices accordingly.
- If Δt>0, it rewinds the integrator to the previous time step.
- Parameters
- ----------
- integrator : ODEIntegrator
- The integrator of the problem.
-"""
+Affect function for the dynamical rank adjustment. It increases the rank of the low-rank manifold by one, and updates the matrices accordingly.
+If Δt>0, it rewinds the integrator to the previous time step.
+
+# Arguments
+
+ - `integrator::ODEIntegrator`: The integrator of the problem.
+=#
function _adjM_affect!(integrator)
ip = integrator.p
opt = ip.opt
@@ -239,8 +235,8 @@ function _adjM_affect!(integrator)
N, M = ip.N, ip.M
@views begin
- z = Δt > 0 ? reshape(ip.u_save[1:N*M], N, M) : reshape(integrator.u[1:N*M], N, M)
- B = Δt > 0 ? reshape(ip.u_save[N*M+1:end], M, M) : reshape(integrator.u[N*M+1:end], M, M)
+ z = Δt > 0 ? reshape(ip.u_save[1:(N*M)], N, M) : reshape(integrator.u[1:(N*M)], N, M)
+ B = Δt > 0 ? reshape(ip.u_save[(N*M+1):end], M, M) : reshape(integrator.u[(N*M+1):end], M, M)
ψ = ip.L_tilde[:, 1]
normalize!(ψ)
@@ -248,7 +244,7 @@ function _adjM_affect!(integrator)
B = cat(B, opt.p0, dims = (1, 2))
resize!(integrator, length(z) + length(B))
integrator.u[1:length(z)] .= z[:]
- integrator.u[length(z)+1:end] .= B[:]
+ integrator.u[(length(z)+1):end] .= B[:]
end
integrator.p = merge(
@@ -265,8 +261,10 @@ function _adjM_affect!(integrator)
),
)
mul!(integrator.p.S, z', z)
- !(opt.compute_Si) &&
- (integrator.p.Si .= _pinv!(Hermitian(integrator.p.S), integrator.temp_MM, integrator.L, atol = opt.atol_inv))
+ !(opt.compute_Si) && (
+ integrator.p.Si .=
+ _pinv_smooth!(Hermitian(integrator.p.S), integrator.temp_MM, integrator.L, atol = opt.atol_inv)
+ )
if Δt > 0
integrator.p = merge(integrator.p, (u_save = copy(integrator.u),))
@@ -282,25 +280,22 @@ function _adjM_affect!(integrator)
end
end
-#=======================================================#
-# DYNAMICAL EVOLUTION EQUATIONS
-#=======================================================#
+#=
+ DYNAMICAL EVOLUTION EQUATIONS
+=#
-@doc raw"""
- dBdz!(du, u, p, t) where T
- Dynamical evolution equations for the low-rank manifold. The function is called by the ODEProblem.
-
- Parameters
- ----------
- du : AbstractVector{T}
- The derivative of the state of the system.
- u : AbstractVector{T}
- The current state of the system.
- p : NamedTuple
- The parameters of the problem.
- t : Real
- The current time.
-"""
+#=
+ dBdz!(du, u, p, t)
+
+Dynamical evolution equations for the low-rank manifold. The function is called by the ODEProblem.
+
+# Arguments
+
+ - `du::AbstractVector`: The derivative of the state vector.
+ - `u::AbstractVector`: The state vector.
+ - `p::NamedTuple`: The parameters of the problem.
+ - `t::Real`: The current time.
+=#
function dBdz!(du, u, p, t)
#NxN
H, Γ = p.H, p.Γ
@@ -313,10 +308,10 @@ function dBdz!(du, u, p, t)
N, M = p.N, p.M
opt = p.opt
- @views z = reshape(u[1:N*M], N, M)
- @views dz = reshape(du[1:N*M], N, M)
- @views B = reshape(u[N*M+1:end], M, M)
- @views dB = reshape(du[N*M+1:end], M, M)
+ @views z = reshape(u[1:(N*M)], N, M)
+ @views dz = reshape(du[1:(N*M)], N, M)
+ @views B = reshape(u[(N*M+1):end], M, M)
+ @views dB = reshape(du[(N*M+1):end], M, M)
#Assign A0 and S
mul!(S, z', z)
@@ -326,8 +321,8 @@ function dBdz!(du, u, p, t)
mul!(A0, z, B)
# Calculate inverse
- opt.compute_Si && (Si .= _pinv!(Hermitian(S), temp_MM, L, atol = opt.atol_inv))
- Bi .= _pinv!(Hermitian(B), temp_MM, L, atol = opt.atol_inv)
+ opt.compute_Si && (Si .= _pinv_smooth!(Hermitian(S), temp_MM, L, atol = opt.atol_inv))
+ Bi .= _pinv_smooth!(Hermitian(B), temp_MM, L, atol = opt.atol_inv)
# Calculate the effective Hamiltonian part of L_tilde
mul!(dz, H, A0)
@@ -360,58 +355,75 @@ function dBdz!(du, u, p, t)
return dB .-= temp_MM
end
-#=======================================================#
-# PROBLEM FORMULATION
-#=======================================================#
+#=
+ OUTPUT GENNERATION
+=#
+
+get_z(u::AbstractArray{T}, N::Integer, M::Integer) where {T} = reshape(view(u, 1:(M*N)), N, M)
+
+get_B(u::AbstractArray{T}, N::Integer, M::Integer) where {T} = reshape(view(u, (M*N+1):length(u)), M, M)
+
+#=
+ PROBLEM FORMULATION
+=#
@doc raw"""
- lr_mesolveProblem(H, z, B, t_l, c_ops; e_ops=(), f_ops=(), opt=LRMesolveOptions(), kwargs...) where T
- Formulates the ODEproblem for the low-rank time evolution of the system. The function is called by lr_mesolve.
-
- Parameters
- ----------
- H : QuantumObject
- The Hamiltonian of the system.
- z : AbstractMatrix{T}
- The initial z matrix.
- B : AbstractMatrix{T}
- The initial B matrix.
- t_l : AbstractVector{T}
- The time steps at which the expectation values and function values are calculated.
- c_ops : AbstractVector{QuantumObject}
- The jump operators of the system.
- e_ops : Tuple{QuantumObject}
- The operators whose expectation values are calculated.
- f_ops : Tuple{Function}
- The functions whose values are calculated.
- opt : LRMesolveOptions
- The options of the problem.
- kwargs : NamedTuple
- Additional keyword arguments for the ODEProblem.
+ lr_mesolveProblem(
+ H::QuantumObject{Operator},
+ z::AbstractArray{T,2},
+ B::AbstractArray{T,2},
+ tlist::AbstractVector,
+ c_ops::Union{AbstractVector,Tuple}=();
+ e_ops::Union{AbstractVector,Tuple}=(),
+ f_ops::Union{AbstractVector,Tuple}=(),
+ opt::NamedTuple = lr_mesolve_options_default,
+ kwargs...,
+ )
+
+Formulates the ODEproblem for the low-rank time evolution of the system. The function is called by [`lr_mesolve`](@ref). For more information about the low-rank master equation, see [gravina2024adaptive](@cite).
+
+# Arguments
+- `H::QuantumObject`: The Hamiltonian of the system.
+- `z::AbstractArray`: The initial z matrix of the low-rank algorithm.
+- `B::AbstractArray`: The initial B matrix of the low-rank algorithm.
+- `tlist::AbstractVector`: The time steps at which the expectation values and function values are calculated.
+- `c_ops::Union{AbstractVector,Tuple}`: The list of the collapse operators.
+- `e_ops::Union{AbstractVector,Tuple}`: The list of the operators for which the expectation values are calculated.
+- `f_ops::Union{AbstractVector,Tuple}`: The list of the functions for which the function values are calculated.
+- `opt::NamedTuple`: The options of the low-rank master equation.
+- `kwargs`: Additional keyword arguments.
"""
function lr_mesolveProblem(
- H::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- z::AbstractArray{T2,2},
- B::AbstractArray{T2,2},
- t_l::AbstractVector,
- c_ops::AbstractVector = [];
- e_ops::Tuple = (),
- f_ops::Tuple = (),
- opt::LRMesolveOptions{AlgType} = LRMesolveOptions(),
+ H::QuantumObject{Operator},
+ z::AbstractArray{T,2},
+ B::AbstractArray{T,2},
+ tlist::AbstractVector,
+ c_ops::Union{AbstractVector,Tuple} = ();
+ e_ops::Union{AbstractVector,Tuple} = (),
+ f_ops::Union{AbstractVector,Tuple} = (),
+ opt::NamedTuple = lr_mesolve_options_default,
kwargs...,
-) where {T1,T2,AlgType<:OrdinaryDiffEqAlgorithm}
+) where {T}
+ Hdims = H.dimensions
# Formulation of problem
- H -= 0.5im * sum([Γ' * Γ for Γ in c_ops])
+ H -= 0.5im * mapreduce(op -> op' * op, +, c_ops)
H = get_data(H)
c_ops = get_data.(c_ops)
e_ops = get_data.(e_ops)
+ t_l = _check_tlist(tlist, _float_type(H))
+
# Initialization of Arrays
expvals = Array{ComplexF64}(undef, length(e_ops), length(t_l))
funvals = Array{ComplexF64}(undef, length(f_ops), length(t_l))
Ml = Array{Int64}(undef, length(t_l))
+ opt = merge(lr_mesolve_options_default, opt)
+ if opt.err_max > 0
+ opt = merge(opt, (is_dynamical = true,))
+ end
+
# Initialization of parameters. Scalars represents in order: Tr(S^{-1}L), t0
p = (
N = size(z, 1),
@@ -421,7 +433,7 @@ function lr_mesolveProblem(
e_ops = e_ops,
f_ops = f_ops,
opt = opt,
- t_l = t_l,
+ times = t_l,
expvals = expvals,
funvals = funvals,
Ml = Ml,
@@ -434,6 +446,7 @@ function lr_mesolveProblem(
Si = similar(B),
u_save = vcat(vec(z), vec(B)),
scalars = [0.0, t_l[1]],
+ Hdims = Hdims,
)
mul!(p.S, z', z)
@@ -482,58 +495,74 @@ function lr_mesolveProblem(
# Initialization of ODEProblem
tspan = (t_l[1], t_l[end])
- return ODEProblem(dBdz!, p.u_save, tspan, p; kwargs2...)
+ return ODEProblem{true}(dBdz!, p.u_save, tspan, p; kwargs2...)
end
+@doc raw"""
+ lr_mesolve(
+ H::QuantumObject{Operator},
+ z::AbstractArray{T,2},
+ B::AbstractArray{T,2},
+ tlist::AbstractVector,
+ c_ops::Union{AbstractVector,Tuple}=();
+ e_ops::Union{AbstractVector,Tuple}=(),
+ f_ops::Union{AbstractVector,Tuple}=(),
+ opt::NamedTuple = lr_mesolve_options_default,
+ kwargs...,
+ )
+
+Time evolution of an open quantum system using the low-rank master equation. For more information about the low-rank master equation, see [gravina2024adaptive](@cite).
+
+# Arguments
+- `H::QuantumObject`: The Hamiltonian of the system.
+- `z::AbstractArray`: The initial z matrix of the low-rank algorithm.
+- `B::AbstractArray`: The initial B matrix of the low-rank algorithm.
+- `tlist::AbstractVector`: The time steps at which the expectation values and function values are calculated.
+- `c_ops::Union{AbstractVector,Tuple}`: The list of the collapse operators.
+- `e_ops::Union{AbstractVector,Tuple}`: The list of the operators for which the expectation values are calculated.
+- `f_ops::Union{AbstractVector,Tuple}`: The list of the functions for which the function values are calculated.
+- `opt::NamedTuple`: The options of the low-rank master equation.
+- `kwargs`: Additional keyword arguments.
+"""
function lr_mesolve(
- H::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
+ H::QuantumObject{Operator},
z::AbstractArray{T2,2},
B::AbstractArray{T2,2},
- t_l::AbstractVector,
- c_ops::AbstractVector = [];
- e_ops::Tuple = (),
- f_ops::Tuple = (),
- opt::LRMesolveOptions{AlgType} = LRMesolveOptions(),
+ tlist::AbstractVector,
+ c_ops::Union{AbstractVector,Tuple} = ();
+ e_ops::Union{AbstractVector,Tuple} = (),
+ f_ops::Union{AbstractVector,Tuple} = (),
+ opt::NamedTuple = lr_mesolve_options_default,
kwargs...,
-) 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...)
+) where {T2}
+ prob = lr_mesolveProblem(H, z, B, tlist, c_ops; e_ops = e_ops, f_ops = f_ops, opt = opt, kwargs...)
return lr_mesolve(prob; kwargs...)
end
-#=======================================================#
-# OUTPUT GENNERATION
-#=======================================================#
-
-get_z(u::AbstractArray{T}, N::Integer, M::Integer) where {T} = reshape(view(u, 1:M*N), N, M)
-
-get_B(u::AbstractArray{T}, N::Integer, M::Integer) where {T} = reshape(view(u, (M*N+1):length(u)), M, M)
-
-@doc raw"""
- lr_mesolve(prob::ODEProblem; kwargs...)
- Solves the ODEProblem formulated by lr_mesolveProblem. The function is called by lr_mesolve.
-
- Parameters
- ----------
- prob : ODEProblem
- The ODEProblem formulated by lr_mesolveProblem.
- kwargs : NamedTuple
- Additional keyword arguments for the ODEProblem.
-"""
function lr_mesolve(prob::ODEProblem; kwargs...)
- sol = solve(prob, prob.p.opt.alg, tstops = prob.p.t_l)
+ sol = solve(prob, prob.p.opt.alg, tstops = prob.p.times)
prob.p.opt.progress && print("\n")
N = prob.p.N
Ll = length.(sol.u)
Ml = @. Int((sqrt(N^2 + 4 * Ll) - N) / 2)
- if !haskey(kwargs, :saveat)
- Bt = map(x -> get_B(x[1], N, x[2]), zip(sol.u, Ml))
- zt = map(x -> get_z(x[1], N, x[2]), zip(sol.u, Ml))
- else
- Bt = get_B(sol.u, N, Ml)
- zt = get_z(sol.u, N, Ml)
- end
-
- return LRTimeEvolutionSol(sol.t, zt, Bt, prob.p.expvals, prob.p.funvals, prob.p.Ml)
+ Bt = map(x -> get_B(x[1], N, x[2]), zip(sol.u, Ml))
+ zt = map(x -> get_z(x[1], N, x[2]), zip(sol.u, Ml))
+ ρt = map(x -> Qobj(x[1] * x[2] * x[1]', type = Operator(), dims = prob.p.Hdims), zip(zt, Bt))
+
+ return TimeEvolutionLRSol(
+ prob.p.times,
+ sol.t,
+ ρt,
+ prob.p.expvals,
+ prob.p.funvals,
+ sol.retcode,
+ prob.p.opt.alg,
+ sol.prob.kwargs[:abstol],
+ sol.prob.kwargs[:reltol],
+ zt,
+ Bt,
+ Ml,
+ )
end
diff --git a/src/time_evolution/mcsolve.jl b/src/time_evolution/mcsolve.jl
index bd62bb410..ef195b591 100644
--- a/src/time_evolution/mcsolve.jl
+++ b/src/time_evolution/mcsolve.jl
@@ -1,115 +1,57 @@
export mcsolveProblem, mcsolveEnsembleProblem, mcsolve
export ContinuousLindbladJumpCallback, DiscreteLindbladJumpCallback
-function _save_func_mcsolve(integrator)
- internal_params = integrator.p
- progr = internal_params.progr_mc
-
- if !internal_params.is_empty_e_ops_mc
- e_ops = internal_params.e_ops_mc
- expvals = internal_params.expvals
- cache_mc = internal_params.cache_mc
-
- copyto!(cache_mc, integrator.u)
- normalize!(cache_mc)
- ψ = cache_mc
- _expect = op -> dot(ψ, op, ψ)
- @. expvals[:, progr.counter[]+1] = _expect(e_ops)
- end
- next!(progr)
- return u_modified!(integrator, false)
-end
+function _mcsolve_prob_func(prob, i, repeat, global_rng, seeds, tlist)
+ seed = seeds[i]
+ traj_rng = typeof(global_rng)()
+ seed!(traj_rng, seed)
-function LindbladJumpAffect!(integrator)
- internal_params = integrator.p
- c_ops = internal_params.c_ops
- cache_mc = internal_params.cache_mc
- weights_mc = internal_params.weights_mc
- cumsum_weights_mc = internal_params.cumsum_weights_mc
- random_n = internal_params.random_n
- jump_times = internal_params.jump_times
- jump_which = internal_params.jump_which
- ψ = integrator.u
-
- @inbounds for i in eachindex(weights_mc)
- mul!(cache_mc, c_ops[i], ψ)
- weights_mc[i] = real(dot(cache_mc, cache_mc))
- end
- cumsum!(cumsum_weights_mc, weights_mc)
- collaps_idx = getindex(1:length(weights_mc), findfirst(>(rand() * sum(weights_mc)), cumsum_weights_mc))
- mul!(cache_mc, c_ops[collaps_idx], ψ)
- normalize!(cache_mc)
- copyto!(integrator.u, cache_mc)
-
- # push!(jump_times, integrator.t)
- # push!(jump_which, collaps_idx)
- random_n[] = rand()
- jump_times[internal_params.jump_times_which_idx[]] = integrator.t
- jump_which[internal_params.jump_times_which_idx[]] = collaps_idx
- internal_params.jump_times_which_idx[] += 1
- if internal_params.jump_times_which_idx[] > length(jump_times)
- resize!(jump_times, length(jump_times) + internal_params.jump_times_which_init_size)
- resize!(jump_which, length(jump_which) + internal_params.jump_times_which_init_size)
- end
-end
+ f = deepcopy(prob.f.f)
+ cb = _mcsolve_initialize_callbacks(prob, tlist, traj_rng)
-LindbladJumpContinuousCondition(u, t, integrator) = integrator.p.random_n[] - real(dot(u, u))
-
-LindbladJumpDiscreteCondition(u, t, integrator) = real(dot(u, u)) < integrator.p.random_n[]
-
-function _mcsolve_prob_func(prob, i, repeat)
- internal_params = prob.p
- seeds = internal_params.seeds
- !isnothing(seeds) && Random.seed!(seeds[i])
-
- prm = merge(
- internal_params,
- (
- expvals = similar(internal_params.expvals),
- cache_mc = similar(internal_params.cache_mc),
- weights_mc = similar(internal_params.weights_mc),
- cumsum_weights_mc = similar(internal_params.weights_mc),
- random_n = Ref(rand()),
- progr_mc = ProgressBar(size(internal_params.expvals, 2), enable = false),
- jump_times_which_idx = Ref(1),
- jump_times = similar(internal_params.jump_times),
- jump_which = similar(internal_params.jump_which),
- ),
- )
-
- return remake(prob, p = prm)
+ return remake(prob, f = f, callback = cb)
end
+# Standard output function
function _mcsolve_output_func(sol, i)
- resize!(sol.prob.p.jump_times, sol.prob.p.jump_times_which_idx[] - 1)
- resize!(sol.prob.p.jump_which, sol.prob.p.jump_times_which_idx[] - 1)
+ idx = _mc_get_jump_callback(sol).affect!.col_times_which_idx[]
+ resize!(_mc_get_jump_callback(sol).affect!.col_times, idx - 1)
+ resize!(_mc_get_jump_callback(sol).affect!.col_which, idx - 1)
return (sol, false)
end
-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(normalize!(sol_i.u[i]), dims = sol_i.prob.p.Hdims) for i in 1:length(sol_i.u)] : nothing
+function _normalize_state!(u, dims, normalize_states)
+ getVal(normalize_states) && normalize!(u)
+ return QuantumObject(u, Ket(), dims)
+end
- copyto!(view(expvals_all, i, :, :), sol_i.prob.p.expvals)
- times[i] = sol_i.t
- jump_times[i] = sol_i.prob.p.jump_times
- return jump_which[i] = sol_i.prob.p.jump_which
+function _mcsolve_make_Heff_QobjEvo(H::QuantumObject, c_ops)
+ c_ops isa Nothing && return QuantumObjectEvolution(H)
+ return QuantumObjectEvolution(H - 1im * mapreduce(op -> op' * op, +, c_ops) / 2)
+end
+function _mcsolve_make_Heff_QobjEvo(H::Tuple, c_ops)
+ c_ops isa Nothing && return QuantumObjectEvolution(H)
+ return QuantumObjectEvolution((H..., -1im * sum(op -> op' * op, c_ops) / 2))
+end
+function _mcsolve_make_Heff_QobjEvo(H::QuantumObjectEvolution, c_ops)
+ c_ops isa Nothing && return H
+ return H + QuantumObjectEvolution(sum(op -> -1im * op' * op / 2, c_ops))
end
@doc raw"""
- mcsolveProblem(H::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
+ mcsolveProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector,
- 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(),
- kwargs...)
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ jump_callback::TJC = ContinuousLindbladJumpCallback(),
+ kwargs...,
+ )
-Generates the ODEProblem for a single trajectory of the Monte Carlo wave function time evolution of an open quantum system.
+Generate the ODEProblem for a single trajectory of the Monte Carlo wave function time evolution of an open quantum system.
Given a system Hamiltonian ``\hat{H}`` and a list of collapse (jump) operators ``\{\hat{C}_n\}_n``, the evolution of the state ``|\psi(t)\rangle`` is governed by the Schrodinger equation:
@@ -145,159 +87,77 @@ If the environmental measurements register a quantum jump, the wave function und
# 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.
-- `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.
-- `jump_callback::LindbladJumpCallbackType`: The Jump Callback type: Discrete or Continuous.
-- `kwargs...`: Additional keyword arguments to pass to the solver.
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``.
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `c_ops`: List of collapse operators ``\{\hat{C}_n\}_n``. It can be either a `Vector` or a `Tuple`.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: Parameters to pass to the solver. This argument is usually expressed as a `NamedTuple` or `AbstractVector` of parameters. For more advanced usage, any custom struct can be used.
+- `rng`: Random number generator for reproducibility.
+- `jump_callback`: The Jump Callback type: Discrete or Continuous. The default is `ContinuousLindbladJumpCallback()`, which is more precise.
+- `kwargs`: The keyword arguments for the ODEProblem.
# 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.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). 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` 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
-- `prob::ODEProblem`: The ODEProblem for the Monte Carlo wave function time evolution.
+- `prob`: The [`TimeEvolutionProblem`](@ref) containing the `ODEProblem` for the Monte Carlo wave function time evolution.
"""
function mcsolveProblem(
- H::QuantumObject{MT1,OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractArray,KetQuantumObject},
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector,
- 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,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
jump_callback::TJC = ContinuousLindbladJumpCallback(),
kwargs...,
-) where {MT1<:AbstractMatrix,TJC<:LindbladJumpCallbackType}
- H.dims != ψ0.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
-
+) where {TJC<:LindbladJumpCallbackType}
haskey(kwargs, :save_idxs) &&
throw(ArgumentError("The keyword argument \"save_idxs\" is not supported in QuantumToolbox."))
c_ops isa Nothing &&
throw(ArgumentError("The list of collapse operators must be provided. Use sesolveProblem instead."))
- t_l = convert(Vector{Float64}, tlist) # Convert it into Float64 to avoid type instabilities for OrdinaryDiffEq.jl
-
- H_eff = H - 1im * mapreduce(op -> op' * op, +, c_ops) / 2
-
- if e_ops isa Nothing
- expvals = Array{ComplexF64}(undef, 0, length(t_l))
- is_empty_e_ops_mc = true
- e_ops2 = MT1[]
- else
- expvals = Array{ComplexF64}(undef, length(e_ops), length(t_l))
- is_empty_e_ops_mc = false
- e_ops2 = get_data.(e_ops)
- end
-
- saveat = e_ops isa Nothing ? t_l : [t_l[end]]
- default_values = (DEFAULT_ODE_SOLVER_OPTIONS..., saveat = saveat)
- kwargs2 = merge(default_values, kwargs)
-
- cache_mc = similar(ψ0.data)
- weights_mc = Array{Float64}(undef, length(c_ops))
- cumsum_weights_mc = similar(weights_mc)
-
- jump_times_which_init_size = 200
- jump_times = Vector{Float64}(undef, jump_times_which_init_size)
- jump_which = Vector{Int16}(undef, jump_times_which_init_size)
-
- params2 = (
- expvals = expvals,
- e_ops_mc = e_ops2,
- is_empty_e_ops_mc = is_empty_e_ops_mc,
- progr_mc = ProgressBar(length(t_l), enable = false),
- seeds = seeds,
- random_n = Ref(rand()),
- c_ops = get_data.(c_ops),
- cache_mc = cache_mc,
- weights_mc = weights_mc,
- cumsum_weights_mc = cumsum_weights_mc,
- jump_times = jump_times,
- jump_which = jump_which,
- jump_times_which_init_size = jump_times_which_init_size,
- jump_times_which_idx = Ref(1),
- params...,
- )
+ tlist = _check_tlist(tlist, _float_type(ψ0))
- return mcsolveProblem(H_eff, ψ0, t_l, alg, H_t, params2, jump_callback; kwargs2...)
-end
+ H_eff_evo = _mcsolve_make_Heff_QobjEvo(H, c_ops)
-function mcsolveProblem(
- H_eff::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
- t_l::AbstractVector,
- alg::OrdinaryDiffEqAlgorithm,
- H_t::Union{Nothing,Function,TimeDependentOperatorSum},
- params::NamedTuple,
- jump_callback::DiscreteLindbladJumpCallback;
- kwargs...,
-) where {T1,T2}
- cb1 = DiscreteCallback(LindbladJumpDiscreteCondition, LindbladJumpAffect!, save_positions = (false, false))
- cb2 = PresetTimeCallback(t_l, _save_func_mcsolve, save_positions = (false, false))
- kwargs2 = (; kwargs...)
- kwargs2 =
- haskey(kwargs2, :callback) ? merge(kwargs2, (callback = CallbackSet(cb1, cb2, kwargs2.callback),)) :
- merge(kwargs2, (callback = CallbackSet(cb1, cb2),))
-
- return sesolveProblem(H_eff, ψ0, t_l; alg = alg, H_t = H_t, params = params, kwargs2...)
-end
+ T = Base.promote_eltype(H_eff_evo, ψ0)
-function mcsolveProblem(
- H_eff::QuantumObject{<:AbstractArray,OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractArray,KetQuantumObject},
- t_l::AbstractVector,
- alg::OrdinaryDiffEqAlgorithm,
- H_t::Union{Nothing,Function,TimeDependentOperatorSum},
- params::NamedTuple,
- jump_callback::ContinuousLindbladJumpCallback;
- kwargs...,
-)
- cb1 = ContinuousCallback(
- LindbladJumpContinuousCondition,
- LindbladJumpAffect!,
- nothing,
- interp_points = jump_callback.interp_points,
- save_positions = (false, false),
- )
- cb2 = PresetTimeCallback(t_l, _save_func_mcsolve, save_positions = (false, false))
- kwargs2 = (; kwargs...)
- kwargs2 =
- haskey(kwargs2, :callback) ? merge(kwargs2, (callback = CallbackSet(cb1, cb2, kwargs2.callback),)) :
- merge(kwargs2, (callback = CallbackSet(cb1, cb2),))
+ # We disable the progress bar of the sesolveProblem because we use a global progress bar for all the trajectories
+ default_values = (DEFAULT_ODE_SOLVER_OPTIONS..., progress_bar = Val(false))
+ kwargs2 = _merge_saveat(tlist, e_ops, default_values; kwargs...)
+ kwargs3 = _generate_mcsolve_kwargs(ψ0, T, e_ops, tlist, c_ops, jump_callback, rng, kwargs2)
- return sesolveProblem(H_eff, ψ0, t_l; alg = alg, H_t = H_t, params = params, kwargs2...)
+ return sesolveProblem(H_eff_evo, ψ0, tlist; params = params, kwargs3...)
end
@doc raw"""
- mcsolveEnsembleProblem(H::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
+ mcsolveEnsembleProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector,
- 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(),
- prob_func::Function=_mcsolve_prob_func,
- output_func::Function=_mcsolve_output_func,
- kwargs...)
-
-Generates the `EnsembleProblem` of `ODEProblem`s for the ensemble of trajectories of the Monte Carlo wave function time evolution of an open quantum system.
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ jump_callback::TJC = ContinuousLindbladJumpCallback(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ prob_func::Union{Function, Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ kwargs...,
+ )
+
+Generate the `EnsembleProblem` of `ODEProblem`s for the ensemble of trajectories of the Monte Carlo wave function time evolution of an open quantum system.
Given a system Hamiltonian ``\hat{H}`` and a list of collapse (jump) operators ``\{\hat{C}_n\}_n``, the evolution of the state ``|\psi(t)\rangle`` is governed by the Schrodinger equation:
@@ -333,79 +193,95 @@ If the environmental measurements register a quantum jump, the wave function und
# 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.
-- `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.
-- `jump_callback::LindbladJumpCallbackType`: The Jump Callback type: Discrete or Continuous.
-- `prob_func::Function`: Function to use for generating the ODEProblem.
-- `output_func::Function`: Function to use for generating the output of a single trajectory.
-- `kwargs...`: Additional keyword arguments to pass to the solver.
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``.
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `c_ops`: List of collapse operators ``\{\hat{C}_n\}_n``. It can be either a `Vector` or a `Tuple`.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: Parameters to pass to the solver. This argument is usually expressed as a `NamedTuple` or `AbstractVector` of parameters. For more advanced usage, any custom struct can be used.
+- `rng`: Random number generator for reproducibility.
+- `ntraj`: Number of trajectories to use.
+- `ensemblealg`: Ensemble algorithm to use. Default to `EnsembleThreads()`.
+- `jump_callback`: The Jump Callback type: Discrete or Continuous. The default is `ContinuousLindbladJumpCallback()`, which is more precise.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `prob_func`: Function to use for generating the ODEProblem.
+- `output_func`: a `Tuple` containing the `Function` to use for generating the output of a single trajectory, the (optional) `ProgressBar` object, and the (optional) `RemoteChannel` object.
+- `kwargs`: The keyword arguments for the ODEProblem.
# 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.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). 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` 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
-- `prob::EnsembleProblem with ODEProblem`: The Ensemble ODEProblem for the Monte Carlo wave function time evolution.
+- `prob`: The [`TimeEvolutionProblem`](@ref) containing the Ensemble `ODEProblem` for the Monte Carlo wave function time evolution.
"""
function mcsolveEnsembleProblem(
- H::QuantumObject{MT1,OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector,
- 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(),
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
jump_callback::TJC = ContinuousLindbladJumpCallback(),
- seeds::Union{Nothing,Vector{Int}} = nothing,
- prob_func::Function = _mcsolve_prob_func,
- output_func::Function = _mcsolve_output_func,
+ progress_bar::Union{Val,Bool} = Val(true),
+ prob_func::Union{Function,Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
kwargs...,
-) where {MT1<:AbstractMatrix,T2,TJC<:LindbladJumpCallbackType}
+) where {TJC<:LindbladJumpCallbackType}
+ _prob_func = isnothing(prob_func) ? _ensemble_dispatch_prob_func(rng, ntraj, tlist, _mcsolve_prob_func) : prob_func
+ _output_func =
+ output_func isa Nothing ?
+ _ensemble_dispatch_output_func(ensemblealg, progress_bar, ntraj, _mcsolve_output_func) : output_func
+
prob_mc = mcsolveProblem(
H,
ψ0,
tlist,
c_ops;
- alg = alg,
e_ops = e_ops,
- H_t = H_t,
params = params,
- seeds = seeds,
+ rng = rng,
jump_callback = jump_callback,
kwargs...,
)
- ensemble_prob = EnsembleProblem(prob_mc, prob_func = prob_func, output_func = output_func, safetycopy = false)
+ ensemble_prob = TimeEvolutionProblem(
+ EnsembleProblem(prob_mc.prob, prob_func = _prob_func, output_func = _output_func[1], safetycopy = false),
+ prob_mc.times,
+ prob_mc.dimensions,
+ (progr = _output_func[2], channel = _output_func[3]),
+ )
return ensemble_prob
end
@doc raw"""
- mcsolve(H::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
+ mcsolve(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector,
- 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,
- ensemble_method=EnsembleThreads(),
- jump_callback::TJC=ContinuousLindbladJumpCallback(),
- kwargs...)
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ alg::OrdinaryDiffEqAlgorithm = Tsit5(),
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ jump_callback::TJC = ContinuousLindbladJumpCallback(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ prob_func::Union{Function, Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ keep_runs_results::Union{Val,Bool} = Val(false),
+ normalize_states::Union{Val,Bool} = Val(true),
+ kwargs...,
+ )
Time evolution of an open quantum system using quantum trajectories.
@@ -443,56 +319,56 @@ If the environmental measurements register a quantum jump, the wave function und
# 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.
-- `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.
-- `n_traj::Int`: Number of trajectories to use.
-- `ensemble_method`: Ensemble method to use.
-- `jump_callback::LindbladJumpCallbackType`: The Jump Callback type: Discrete or Continuous.
-- `prob_func::Function`: Function to use for generating the ODEProblem.
-- `output_func::Function`: Function to use for generating the output of a single trajectory.
-- `kwargs...`: Additional keyword arguments to pass to the solver.
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``.
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `c_ops`: List of collapse operators ``\{\hat{C}_n\}_n``. It can be either a `Vector` or a `Tuple`.
+- `alg`: The algorithm to use for the ODE solver. Default to `Tsit5()`.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: Parameters to pass to the solver. This argument is usually expressed as a `NamedTuple` or `AbstractVector` of parameters. For more advanced usage, any custom struct can be used.
+- `rng`: Random number generator for reproducibility.
+- `ntraj`: Number of trajectories to use.
+- `ensemblealg`: Ensemble algorithm to use. Default to `EnsembleThreads()`.
+- `jump_callback`: The Jump Callback type: Discrete or Continuous. The default is `ContinuousLindbladJumpCallback()`, which is more precise.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `prob_func`: Function to use for generating the ODEProblem.
+- `output_func`: a `Tuple` containing the `Function` to use for generating the output of a single trajectory, the (optional) `ProgressBar` object, and the (optional) `RemoteChannel` object.
+- `keep_runs_results`: Whether to save the results of each trajectory. Default to `Val(false)`.
+- `normalize_states`: Whether to normalize the states. Default to `Val(true)`.
+- `kwargs`: The keyword arguments for the ODEProblem.
# Notes
-- `ensemble_method` can be one of `EnsembleThreads()`, `EnsembleSerial()`, `EnsembleDistributed()`
+- `ensemblealg` 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.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). 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` 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
-- `sol::TimeEvolutionMCSol`: The solution of the time evolution. See also [`TimeEvolutionMCSol`](@ref)
+- `sol::TimeEvolutionMCSol`: The solution of the time evolution. See also [`TimeEvolutionMCSol`](@ref).
"""
function mcsolve(
- H::QuantumObject{MT1,OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject},
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector,
- c_ops::Union{Nothing,AbstractVector} = nothing;
+ c_ops::Union{Nothing,AbstractVector,Tuple} = 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,
- n_traj::Int = 1,
- ensemble_method = EnsembleThreads(),
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
jump_callback::TJC = ContinuousLindbladJumpCallback(),
- prob_func::Function = _mcsolve_prob_func,
- output_func::Function = _mcsolve_output_func,
+ progress_bar::Union{Val,Bool} = Val(true),
+ prob_func::Union{Function,Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ keep_runs_results::Union{Val,Bool} = Val(false),
+ normalize_states::Union{Val,Bool} = Val(true),
kwargs...,
-) 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
-
+) where {TJC<:LindbladJumpCallbackType}
ens_prob_mc = mcsolveEnsembleProblem(
H,
ψ0,
@@ -500,52 +376,57 @@ function mcsolve(
c_ops;
alg = alg,
e_ops = e_ops,
- H_t = H_t,
params = params,
- seeds = seeds,
+ rng = rng,
+ ntraj = ntraj,
+ ensemblealg = ensemblealg,
jump_callback = jump_callback,
+ progress_bar = progress_bar,
prob_func = prob_func,
output_func = output_func,
kwargs...,
)
- return mcsolve(ens_prob_mc; alg = alg, n_traj = n_traj, ensemble_method = ensemble_method)
+ return mcsolve(ens_prob_mc, alg, ntraj, ensemblealg, makeVal(keep_runs_results), normalize_states)
end
function mcsolve(
- ens_prob_mc::EnsembleProblem;
+ ens_prob_mc::TimeEvolutionProblem,
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- n_traj::Int = 1,
- ensemble_method = EnsembleThreads(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ keep_runs_results = Val(false),
+ normalize_states = Val(true),
)
- sol = solve(ens_prob_mc, alg, ensemble_method, trajectories = n_traj)
+ sol = _ensemble_dispatch_solve(ens_prob_mc, alg, ensemblealg, ntraj)
+
+ dims = ens_prob_mc.dimensions
_sol_1 = sol[:, 1]
+ _expvals_sol_1 = _get_expvals(_sol_1, SaveFuncMCSolve)
- expvals_all = Array{ComplexF64}(undef, length(sol), size(_sol_1.prob.p.expvals)...)
- times = Vector{Vector{Float64}}(undef, length(sol))
- states =
- isempty(_sol_1.prob.kwargs[:saveat]) ? fill(QuantumObject[], length(sol)) :
- Vector{Vector{QuantumObject}}(undef, length(sol))
- jump_times = Vector{Vector{Float64}}(undef, length(sol))
- jump_which = Vector{Vector{Int16}}(undef, length(sol))
-
- foreach(
- 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)
+ _expvals_all =
+ _expvals_sol_1 isa Nothing ? nothing : map(i -> _get_expvals(sol[:, i], SaveFuncMCSolve), eachindex(sol))
+ expvals_all = _expvals_all isa Nothing ? nothing : stack(_expvals_all, dims = 2) # Stack on dimension 2 to align with QuTiP
+
+ # stack to transform Vector{Vector{QuantumObject}} -> Matrix{QuantumObject}
+ states_all = stack(map(i -> _normalize_state!.(sol[:, i].u, Ref(dims), normalize_states), eachindex(sol)), dims = 1)
+
+ col_times = map(i -> _mc_get_jump_callback(sol[:, i]).affect!.col_times, eachindex(sol))
+ col_which = map(i -> _mc_get_jump_callback(sol[:, i]).affect!.col_which, eachindex(sol))
+
+ kwargs = NamedTuple(_sol_1.prob.kwargs) # Convert to NamedTuple for Zygote.jl compatibility
return TimeEvolutionMCSol(
- n_traj,
- times,
- states,
- expvals,
- expvals_all,
- jump_times,
- jump_which,
+ ntraj,
+ ens_prob_mc.times,
+ _sol_1.t,
+ _store_multitraj_states(states_all, keep_runs_results),
+ _store_multitraj_expect(expvals_all, keep_runs_results),
+ col_times,
+ col_which,
sol.converged,
_sol_1.alg,
- _sol_1.prob.kwargs[:abstol],
- _sol_1.prob.kwargs[:reltol],
+ kwargs.abstol,
+ kwargs.reltol,
)
end
diff --git a/src/time_evolution/mesolve.jl b/src/time_evolution/mesolve.jl
index 404ec4947..65eac4eae 100644
--- a/src/time_evolution/mesolve.jl
+++ b/src/time_evolution/mesolve.jl
@@ -1,96 +1,53 @@
export mesolveProblem, mesolve
-function _save_func_mesolve(integrator)
- internal_params = integrator.p
- progr = internal_params.progr
-
- if !internal_params.is_empty_e_ops
- expvals = internal_params.expvals
- e_ops = internal_params.e_ops
- # This is equivalent to tr(op * ρ), when both are matrices.
- # The advantage of using this convention is that I don't need
- # to reshape u to make it a matrix, but I reshape the e_ops once.
-
- ρ = integrator.u
- _expect = op -> dot(op, ρ)
- @. expvals[:, progr.counter[]+1] = _expect(e_ops)
- end
- next!(progr)
- return u_modified!(integrator, false)
-end
-
-mesolve_ti_dudt!(du, u, p, t) = mul!(du, p.L, u)
-function mesolve_td_dudt!(du, u, p, t)
- mul!(du, p.L, u)
- L_t = p.H_t(t, p)
- return mul!(du, L_t, u, 1, 1)
-end
-
-_generate_mesolve_e_op(op) = mat2vec(adjoint(get_data(op)))
-
-function _generate_mesolve_kwargs_with_callback(t_l, kwargs)
- cb1 = PresetTimeCallback(t_l, _save_func_mesolve, save_positions = (false, false))
- kwargs2 =
- haskey(kwargs, :callback) ? merge(kwargs, (callback = CallbackSet(kwargs.callback, cb1),)) :
- merge(kwargs, (callback = cb1,))
-
- return kwargs2
-end
-
-function _generate_mesolve_kwargs(e_ops, progress_bar::Val{true}, t_l, kwargs)
- return _generate_mesolve_kwargs_with_callback(t_l, kwargs)
-end
-
-function _generate_mesolve_kwargs(e_ops, progress_bar::Val{false}, t_l, kwargs)
- if e_ops isa Nothing
- return kwargs
- end
- return _generate_mesolve_kwargs_with_callback(t_l, kwargs)
-end
+_mesolve_make_L_QobjEvo(H::Union{QuantumObject,Nothing}, c_ops) = QobjEvo(liouvillian(H, c_ops); type = SuperOperator())
+_mesolve_make_L_QobjEvo(H::Union{QuantumObjectEvolution,Tuple}, c_ops) = liouvillian(QobjEvo(H), c_ops)
+_mesolve_make_L_QobjEvo(H::Nothing, c_ops::Nothing) = throw(ArgumentError("Both H and
+c_ops are Nothing. You are probably running the wrong function."))
@doc raw"""
- mesolveProblem(H::QuantumObject,
+ mesolveProblem(
+ H::Union{AbstractQuantumObject,Tuple},
ψ0::QuantumObject,
- tlist::AbstractVector,
- 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(),
- progress_bar::Union{Val,Bool}=Val(true),
- kwargs...)
+ tlist,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ inplace::Union{Val,Bool} = Val(true),
+ kwargs...,
+ )
-Generates the ODEProblem for the master equation time evolution of an open quantum system:
+Generate 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
-- `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::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.
-- `progress_bar::Union{Val,Bool}=Val(true)`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
-- `kwargs...`: The keyword arguments for the ODEProblem.
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``. It can be either a [`Ket`](@ref), [`Operator`](@ref) or [`OperatorKet`](@ref).
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `c_ops`: List of collapse operators ``\{\hat{C}_n\}_n``. It can be either a `Vector` or a `Tuple`.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: Parameters to pass to the solver. This argument is usually expressed as a `NamedTuple` or `AbstractVector` of parameters. For more advanced usage, any custom struct can be used.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `inplace`: Whether to use the inplace version of the ODEProblem. The default is `Val(true)`. It is recommended to use `Val(true)` for better performance, but it is sometimes necessary to use `Val(false)`, for example when performing automatic differentiation using [Zygote.jl](https://github.com/FluxML/Zygote.jl).
+- `kwargs`: The keyword arguments for the ODEProblem.
# 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.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). You can also specify `e_ops` and `saveat` separately.
+- If `H` is an [`Operator`](@ref), `ψ0` is a [`Ket`](@ref) and `c_ops` is `Nothing`, the function will call [`sesolveProblem`](@ref) instead.
- The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`.
-- 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
@@ -98,110 +55,97 @@ where
- `prob::ODEProblem`: The ODEProblem for the master equation time evolution.
"""
function mesolveProblem(
- H::QuantumObject{MT1,HOpType},
- ψ0::QuantumObject{<:AbstractArray{T2},StateOpType},
+ H::Union{AbstractQuantumObject{HOpType},Tuple},
+ ψ0::QuantumObject{StateOpType},
tlist,
- 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(),
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
progress_bar::Union{Val,Bool} = Val(true),
+ inplace::Union{Val,Bool} = Val(true),
kwargs...,
-) where {
- MT1<:AbstractMatrix,
- T2,
- HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},
-}
- H.dims != ψ0.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension."))
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator,OperatorKet}}
+ (isoper(H) && isket(ψ0) && isnothing(c_ops)) && return sesolveProblem(
+ H,
+ ψ0,
+ tlist;
+ e_ops = e_ops,
+ params = params,
+ progress_bar = progress_bar,
+ inplace = inplace,
+ kwargs...,
+ )
haskey(kwargs, :save_idxs) &&
throw(ArgumentError("The keyword argument \"save_idxs\" is not supported in QuantumToolbox."))
- 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
+ tlist = _check_tlist(tlist, _float_type(ψ0))
- ρ0 = mat2vec(ket2dm(ψ0).data)
+ L_evo = _mesolve_make_L_QobjEvo(H, c_ops)
+ check_dimensions(L_evo, ψ0)
- L = liouvillian(H, c_ops).data
- 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 = mat2vec(MT1)[]
- is_empty_e_ops = true
+ T = Base.promote_eltype(L_evo, ψ0)
+ ρ0 = if isoperket(ψ0) # Convert it to dense vector with complex element type
+ to_dense(_complex_float_type(T), copy(ψ0.data))
else
- expvals = Array{ComplexF64}(undef, length(e_ops), length(t_l))
- e_ops2 = [_generate_mesolve_e_op(op) for op in e_ops]
- is_empty_e_ops = isempty(e_ops)
+ to_dense(_complex_float_type(T), mat2vec(ket2dm(ψ0).data))
end
+ L = L_evo.data
- p = (
- L = L,
- progr = progr,
- Hdims = H.dims,
- e_ops = e_ops2,
- expvals = expvals,
- H_t = H_t,
- is_empty_e_ops = is_empty_e_ops,
- params...,
- )
+ kwargs2 = _merge_saveat(tlist, e_ops, DEFAULT_ODE_SOLVER_OPTIONS; kwargs...)
+ kwargs3 = _generate_se_me_kwargs(e_ops, makeVal(progress_bar), tlist, kwargs2, SaveFuncMESolve)
- saveat = e_ops isa Nothing ? t_l : [t_l[end]]
- default_values = (DEFAULT_ODE_SOLVER_OPTIONS..., saveat = saveat)
- kwargs2 = merge(default_values, kwargs)
- kwargs3 = _generate_mesolve_kwargs(e_ops, progress_bar_val, t_l, kwargs2)
+ tspan = (tlist[1], tlist[end])
- dudt! = is_time_dependent ? mesolve_td_dudt! : mesolve_ti_dudt!
+ prob = ODEProblem{getVal(inplace),FullSpecialize}(L, ρ0, tspan, params; kwargs3...)
- tspan = (t_l[1], t_l[end])
- return ODEProblem{true,FullSpecialize}(dudt!, ρ0, tspan, p; kwargs3...)
+ return TimeEvolutionProblem(prob, tlist, L_evo.dimensions, (isoperket = Val(isoperket(ψ0)),))
end
@doc raw"""
- mesolve(H::QuantumObject,
+ mesolve(
+ H::Union{AbstractQuantumObject,Tuple},
ψ0::QuantumObject,
- tlist::AbstractVector,
- 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(),
- progress_bar::Union{Val,Bool}=Val(true),
- kwargs...)
+ tlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
+ alg::OrdinaryDiffEqAlgorithm = Tsit5(),
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ inplace::Union{Val,Bool} = Val(true),
+ kwargs...,
+ )
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
-- `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::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.
-- `params::NamedTuple`: Named Tuple of parameters to pass to the solver.
-- `progress_bar::Union{Val,Bool}`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
-- `kwargs...`: Additional keyword arguments to pass to the solver.
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``. It can be either a [`Ket`](@ref), [`Operator`](@ref) or [`OperatorKet`](@ref).
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `c_ops`: List of collapse operators ``\{\hat{C}_n\}_n``. It can be either a `Vector` or a `Tuple`.
+- `alg`: The algorithm for the ODE solver. The default value is `Tsit5()`.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: Parameters to pass to the solver. This argument is usually expressed as a `NamedTuple` or `AbstractVector` of parameters. For more advanced usage, any custom struct can be used.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `inplace`: Whether to use the inplace version of the ODEProblem. The default is `Val(true)`. It is recommended to use `Val(true)` for better performance, but it is sometimes necessary to use `Val(false)`, for example when performing automatic differentiation using [Zygote.jl](https://github.com/FluxML/Zygote.jl).
+- `kwargs`: The keyword arguments for the ODEProblem.
# 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.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). You can also specify `e_ops` and `saveat` separately.
+- If `H` is an [`Operator`](@ref), `ψ0` is a [`Ket`](@ref) and `c_ops` is `Nothing`, the function will call [`sesolve`](@ref) instead.
- The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`.
- 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/)
@@ -211,22 +155,34 @@ where
- `sol::TimeEvolutionSol`: The solution of the time evolution. See also [`TimeEvolutionSol`](@ref)
"""
function mesolve(
- H::QuantumObject{MT1,HOpType},
- ψ0::QuantumObject{<:AbstractArray{T2},StateOpType},
+ H::Union{AbstractQuantumObject{HOpType},Tuple},
+ ψ0::QuantumObject{StateOpType},
tlist::AbstractVector,
- c_ops::Union{Nothing,AbstractVector} = nothing;
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Union{Nothing,AbstractVector} = nothing,
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
- params::NamedTuple = NamedTuple(),
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
progress_bar::Union{Val,Bool} = Val(true),
+ inplace::Union{Val,Bool} = Val(true),
kwargs...,
-) where {
- MT1<:AbstractMatrix,
- T2,
- HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},
-}
+) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator,OperatorKet}}
+ (isoper(H) && isket(ψ0) && isnothing(c_ops)) && return sesolve(
+ H,
+ ψ0,
+ tlist;
+ alg = alg,
+ e_ops = e_ops,
+ params = params,
+ progress_bar = progress_bar,
+ inplace = inplace,
+ kwargs...,
+ )
+
+ # Move sensealg argument to solve for Enzyme.jl support.
+ # TODO: Remove it when https://github.com/SciML/SciMLSensitivity.jl/issues/1225 is fixed.
+ sensealg = get(kwargs, :sensealg, nothing)
+ kwargs_filtered = isnothing(sensealg) ? kwargs : Base.structdiff((; kwargs...), (sensealg = sensealg,))
+
prob = mesolveProblem(
H,
ψ0,
@@ -234,27 +190,40 @@ function mesolve(
c_ops;
alg = alg,
e_ops = e_ops,
- H_t = H_t,
params = params,
- progress_bar = makeVal(progress_bar),
- kwargs...,
+ progress_bar = progress_bar,
+ inplace = inplace,
+ kwargs_filtered...,
)
- return mesolve(prob, alg)
+ # TODO: Remove sensealg when https://github.com/SciML/SciMLSensitivity.jl/issues/1225 is fixed
+ if isnothing(sensealg)
+ return mesolve(prob, alg)
+ else
+ return mesolve(prob, alg; sensealg = sensealg)
+ end
end
-function mesolve(prob::ODEProblem, alg::OrdinaryDiffEqAlgorithm = Tsit5())
- sol = solve(prob, alg)
+function mesolve(prob::TimeEvolutionProblem, alg::OrdinaryDiffEqAlgorithm = Tsit5(); kwargs...)
+ sol = solve(prob.prob, alg; kwargs...)
+
+ # No type instabilities since `isoperket` is a Val, and so it is known at compile time
+ if getVal(prob.kwargs.isoperket)
+ ρt = map(ϕ -> QuantumObject(ϕ, type = OperatorKet(), dims = prob.dimensions), sol.u)
+ else
+ ρt = map(ϕ -> QuantumObject(vec2mat(ϕ), type = Operator(), dims = prob.dimensions), sol.u)
+ end
- ρt = map(ϕ -> QuantumObject(vec2mat(ϕ), type = Operator, dims = sol.prob.p.Hdims), sol.u)
+ kwargs = NamedTuple(sol.prob.kwargs) # Convert to NamedTuple for Zygote.jl compatibility
return TimeEvolutionSol(
+ prob.times,
sol.t,
ρt,
- sol.prob.p.expvals,
+ _get_expvals(sol, SaveFuncMESolve),
sol.retcode,
sol.alg,
- sol.prob.kwargs[:abstol],
- sol.prob.kwargs[:reltol],
+ kwargs.abstol,
+ kwargs.reltol,
)
end
diff --git a/src/time_evolution/sesolve.jl b/src/time_evolution/sesolve.jl
index 6ef530d65..3ad229402 100644
--- a/src/time_evolution/sesolve.jl
+++ b/src/time_evolution/sesolve.jl
@@ -1,60 +1,20 @@
export sesolveProblem, sesolve
-function _save_func_sesolve(integrator)
- internal_params = integrator.p
- progr = internal_params.progr
-
- if !internal_params.is_empty_e_ops
- e_ops = internal_params.e_ops
- expvals = internal_params.expvals
-
- ψ = integrator.u
- _expect = op -> dot(ψ, op, ψ)
- @. expvals[:, progr.counter[]+1] = _expect(e_ops)
- end
- next!(progr)
- return u_modified!(integrator, false)
-end
-
-sesolve_ti_dudt!(du, u, p, t) = mul!(du, p.U, u)
-function sesolve_td_dudt!(du, u, p, t)
- mul!(du, p.U, u)
- H_t = p.H_t(t, p)
- return mul!(du, H_t, u, -1im, 1)
-end
-
-function _generate_sesolve_kwargs_with_callback(t_l, kwargs)
- cb1 = PresetTimeCallback(t_l, _save_func_sesolve, save_positions = (false, false))
- kwargs2 =
- haskey(kwargs, :callback) ? merge(kwargs, (callback = CallbackSet(kwargs.callback, cb1),)) :
- merge(kwargs, (callback = cb1,))
-
- return kwargs2
-end
-
-function _generate_sesolve_kwargs(e_ops, progress_bar::Val{true}, t_l, kwargs)
- return _generate_sesolve_kwargs_with_callback(t_l, kwargs)
-end
-
-function _generate_sesolve_kwargs(e_ops, progress_bar::Val{false}, t_l, kwargs)
- if e_ops isa Nothing
- return kwargs
- end
- return _generate_sesolve_kwargs_with_callback(t_l, kwargs)
-end
+_sesolve_make_U_QobjEvo(H) = -1im * QuantumObjectEvolution(H, type = Operator())
@doc raw"""
- sesolveProblem(H::QuantumObject,
- ψ0::QuantumObject,
+ sesolveProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector;
- alg::OrdinaryDiffEqAlgorithm=Tsit5()
- e_ops::Union{Nothing,AbstractVector} = nothing,
- H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing,
- params::NamedTuple=NamedTuple(),
- progress_bar::Union{Val,Bool}=Val(true),
- kwargs...)
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ inplace::Union{Val,Bool} = Val(true),
+ kwargs...,
+ )
-Generates the ODEProblem for the Schrödinger time evolution of a quantum system:
+Generate the ODEProblem for the Schrödinger time evolution of a quantum system:
```math
\frac{\partial}{\partial t} |\psi(t)\rangle = -i \hat{H} |\psi(t)\rangle
@@ -62,96 +22,71 @@ Generates the ODEProblem for the Schrödinger time evolution of a quantum system
# 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.
-- `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.
-- `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.
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``.
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: Parameters to pass to the solver. This argument is usually expressed as a `NamedTuple` or `AbstractVector` of parameters. For more advanced usage, any custom struct can be used.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `inplace`: Whether to use the inplace version of the ODEProblem. The default is `Val(true)`. It is recommended to use `Val(true)` for better performance, but it is sometimes necessary to use `Val(false)`, for example when performing automatic differentiation using [Zygote.jl](https://github.com/FluxML/Zygote.jl).
+- `kwargs`: The keyword arguments for the ODEProblem.
# 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.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). 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` 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
-- `prob`: The `ODEProblem` for the Schrödinger time evolution of the system.
+- `prob`: The [`TimeEvolutionProblem`](@ref) containing the `ODEProblem` for the Schrödinger time evolution of the system.
"""
function sesolveProblem(
- H::QuantumObject{MT1,OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractVector{T2},KetQuantumObject},
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector;
- alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Union{Nothing,AbstractVector} = nothing,
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
- params::NamedTuple = NamedTuple(),
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
progress_bar::Union{Val,Bool} = Val(true),
+ inplace::Union{Val,Bool} = Val(true),
kwargs...,
-) where {MT1<:AbstractMatrix,T2}
- 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."))
- 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
+ tlist = _check_tlist(tlist, _float_type(ψ0))
- ϕ0 = get_data(ψ0)
+ H_evo = _sesolve_make_U_QobjEvo(H) # Multiply by -i
+ isoper(H_evo) || throw(ArgumentError("The Hamiltonian must be an Operator."))
+ check_dimensions(H_evo, ψ0)
- U = -1im * get_data(H)
- progr = ProgressBar(length(t_l), enable = getVal(progress_bar_val))
+ T = Base.promote_eltype(H_evo, ψ0)
+ ψ0 = to_dense(_complex_float_type(T), get_data(ψ0)) # Convert it to dense vector with complex element type
+ U = H_evo.data
- 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 = (
- U = U,
- e_ops = e_ops2,
- expvals = expvals,
- progr = progr,
- Hdims = H.dims,
- H_t = H_t,
- is_empty_e_ops = is_empty_e_ops,
- params...,
- )
+ kwargs2 = _merge_saveat(tlist, e_ops, DEFAULT_ODE_SOLVER_OPTIONS; kwargs...)
+ kwargs3 = _generate_se_me_kwargs(e_ops, makeVal(progress_bar), tlist, kwargs2, SaveFuncSESolve)
- saveat = e_ops isa Nothing ? t_l : [t_l[end]]
- default_values = (DEFAULT_ODE_SOLVER_OPTIONS..., saveat = saveat)
- kwargs2 = merge(default_values, kwargs)
- kwargs3 = _generate_sesolve_kwargs(e_ops, progress_bar_val, t_l, kwargs2)
+ tspan = (tlist[1], tlist[end])
- dudt! = is_time_dependent ? sesolve_td_dudt! : sesolve_ti_dudt!
+ prob = ODEProblem{getVal(inplace),FullSpecialize}(U, ψ0, tspan, params; kwargs3...)
- tspan = (t_l[1], t_l[end])
- return ODEProblem{true,FullSpecialize}(dudt!, ϕ0, tspan, p; kwargs3...)
+ return TimeEvolutionProblem(prob, tlist, H_evo.dimensions)
end
@doc raw"""
- sesolve(H::QuantumObject,
- ψ0::QuantumObject,
+ sesolve(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector;
- alg::OrdinaryDiffEqAlgorithm=Tsit5(),
- e_ops::Union{Nothing,AbstractVector} = nothing,
- H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing,
- params::NamedTuple=NamedTuple(),
- progress_bar::Union{Val,Bool}=Val(true),
- kwargs...)
+ alg::OrdinaryDiffEqAlgorithm = Tsit5(),
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ inplace::Union{Val,Bool} = Val(true),
+ kwargs...,
+ )
Time evolution of a closed quantum system using the Schrödinger equation:
@@ -161,20 +96,20 @@ Time evolution of a closed quantum system using the Schrödinger equation:
# Arguments
-- `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::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.
-- `progress_bar::Union{Val,Bool}`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
-- `kwargs...`: Additional keyword arguments to pass to the solver.
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``.
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `alg`: The algorithm for the ODE solver. The default is `Tsit5()`.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: Parameters to pass to the solver. This argument is usually expressed as a `NamedTuple` or `AbstractVector` of parameters. For more advanced usage, any custom struct can be used.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `inplace`: Whether to use the inplace version of the ODEProblem. The default is `Val(true)`. It is recommended to use `Val(true)` for better performance, but it is sometimes necessary to use `Val(false)`, for example when performing automatic differentiation using [Zygote.jl](https://github.com/FluxML/Zygote.jl).
+- `kwargs`: The keyword arguments for the ODEProblem.
# 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.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). 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` 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/)
@@ -184,43 +119,56 @@ Time evolution of a closed quantum system using the Schrödinger equation:
- `sol::TimeEvolutionSol`: The solution of the time evolution. See also [`TimeEvolutionSol`](@ref)
"""
function sesolve(
- H::QuantumObject{MT1,OperatorQuantumObject},
- ψ0::QuantumObject{<:AbstractVector{T2},KetQuantumObject},
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
tlist::AbstractVector;
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Union{Nothing,AbstractVector} = nothing,
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
- params::NamedTuple = NamedTuple(),
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
progress_bar::Union{Val,Bool} = Val(true),
+ inplace::Union{Val,Bool} = Val(true),
kwargs...,
-) where {MT1<:AbstractMatrix,T2}
+)
+
+ # Move sensealg argument to solve for Enzyme.jl support.
+ # TODO: Remove it when https://github.com/SciML/SciMLSensitivity.jl/issues/1225 is fixed.
+ sensealg = get(kwargs, :sensealg, nothing)
+ kwargs_filtered = isnothing(sensealg) ? kwargs : Base.structdiff((; kwargs...), (sensealg = sensealg,))
+
prob = sesolveProblem(
H,
ψ0,
tlist;
- alg = alg,
e_ops = e_ops,
- H_t = H_t,
params = params,
- progress_bar = makeVal(progress_bar),
- kwargs...,
+ progress_bar = progress_bar,
+ inplace = inplace,
+ kwargs_filtered...,
)
- return sesolve(prob, alg)
+ # TODO: Remove it when https://github.com/SciML/SciMLSensitivity.jl/issues/1225 is fixed.
+ if isnothing(sensealg)
+ return sesolve(prob, alg)
+ else
+ return sesolve(prob, alg; sensealg = sensealg)
+ end
end
-function sesolve(prob::ODEProblem, alg::OrdinaryDiffEqAlgorithm = Tsit5())
- sol = solve(prob, alg)
+function sesolve(prob::TimeEvolutionProblem, alg::OrdinaryDiffEqAlgorithm = Tsit5(); kwargs...)
+ sol = solve(prob.prob, alg; kwargs...)
+
+ ψt = map(ϕ -> QuantumObject(ϕ, type = Ket(), dims = prob.dimensions), sol.u)
- ψt = map(ϕ -> QuantumObject(ϕ, type = Ket, dims = sol.prob.p.Hdims), sol.u)
+ kwargs = NamedTuple(sol.prob.kwargs) # Convert to NamedTuple for Zygote.jl compatibility
return TimeEvolutionSol(
+ prob.times,
sol.t,
ψt,
- sol.prob.p.expvals,
+ _get_expvals(sol, SaveFuncSESolve),
sol.retcode,
sol.alg,
- sol.prob.kwargs[:abstol],
- sol.prob.kwargs[:reltol],
+ kwargs.abstol,
+ kwargs.reltol,
)
end
diff --git a/src/time_evolution/smesolve.jl b/src/time_evolution/smesolve.jl
new file mode 100644
index 000000000..2c969f7bc
--- /dev/null
+++ b/src/time_evolution/smesolve.jl
@@ -0,0 +1,443 @@
+export smesolveProblem, smesolveEnsembleProblem, smesolve
+
+_smesolve_generate_state(u, dims, isoperket::Val{false}) = QuantumObject(vec2mat(u), type = Operator(), dims = dims)
+_smesolve_generate_state(u, dims, isoperket::Val{true}) = QuantumObject(u, type = OperatorKet(), dims = dims)
+
+function _smesolve_update_coeff(u, p, t, op_vec)
+ return 2 * real(dot(op_vec, u)) #this is Tr[Sn * ρ + ρ * Sn']
+end
+
+_smesolve_ScalarOperator(op_vec) =
+ ScalarOperator(one(eltype(op_vec)), (a, u, p, t) -> -_smesolve_update_coeff(u, p, t, op_vec))
+
+@doc raw"""
+ smesolveProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject,
+ tlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ store_measurement::Union{Val, Bool} = Val(false),
+ kwargs...,
+ )
+
+Generate the SDEProblem for the Stochastic Master Equation time evolution of an open quantum system. This is defined by the following stochastic differential equation:
+
+```math
+d \rho (t) = -i [\hat{H}, \rho(t)] dt + \sum_i \mathcal{D}[\hat{C}_i] \rho(t) dt + \sum_n \mathcal{D}[\hat{S}_n] \rho(t) dt + \sum_n \mathcal{H}[\hat{S}_n] \rho(t) dW_n(t),
+```
+
+where
+
+```math
+\mathcal{D}[\hat{O}] \rho = \hat{O} \rho \hat{O}^\dagger - \frac{1}{2} \{\hat{O}^\dagger \hat{O}, \rho\},
+```
+
+is the Lindblad superoperator, and
+
+```math
+\mathcal{H}[\hat{O}] \rho = \hat{O} \rho + \rho \hat{O}^\dagger - \mathrm{Tr}[\hat{O} \rho + \rho \hat{O}^\dagger] \rho,
+```
+
+Above, ``\hat{C}_i`` represent the collapse operators related to pure dissipation, while ``\hat{S}_n`` are the stochastic collapse operators. The ``dW_n(t)`` term is the real Wiener increment associated to ``\hat{S}_n``. See [Wiseman2009Quantum](@cite) for more details.
+
+# Arguments
+
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``. It can be either a [`Ket`](@ref), [`Operator`](@ref) or [`OperatorKet`](@ref).
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `c_ops`: List of collapse operators ``\{\hat{C}_i\}_i``. It can be either a `Vector` or a `Tuple`.
+- `sc_ops`: List of stochastic collapse operators ``\{\hat{S}_n\}_n``. It can be either a `Vector`, a `Tuple` or a [`AbstractQuantumObject`](@ref). It is recommended to use the last case when only one operator is provided.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: `NullParameters` of parameters to pass to the solver.
+- `rng`: Random number generator for reproducibility.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `store_measurement`: Whether to store the measurement expectation values. Default is `Val(false)`.
+- `kwargs`: The keyword arguments for the ODEProblem.
+
+# Notes
+
+- The states will be saved depend on the keyword argument `saveat` in `kwargs`.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). You can also specify `e_ops` and `saveat` separately.
+- The default tolerances in `kwargs` are given as `reltol=1e-2` and `abstol=1e-2`.
+- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/)
+
+!!! tip "Performance Tip"
+ When `sc_ops` contains only a single operator, it is recommended to pass only that operator as the argument. This ensures that the stochastic noise is diagonal, making the simulation faster.
+
+# Returns
+
+- `prob`: The [`TimeEvolutionProblem`](@ref) containing the `SDEProblem` for the Stochastic Master Equation time evolution.
+"""
+function smesolveProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{StateOpType},
+ tlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+) where {StateOpType<:Union{Ket,Operator,OperatorKet}}
+ haskey(kwargs, :save_idxs) &&
+ throw(ArgumentError("The keyword argument \"save_idxs\" is not supported in QuantumToolbox."))
+
+ isnothing(sc_ops) &&
+ throw(ArgumentError("The list of stochastic collapse operators must be provided. Use mesolveProblem instead."))
+ sc_ops_list = _make_c_ops_list(sc_ops) # If it is an AbstractQuantumObject but we need to iterate
+ sc_ops_isa_Qobj = sc_ops isa AbstractQuantumObject # We can avoid using non-diagonal noise if sc_ops is just an AbstractQuantumObject
+
+ tlist = _check_tlist(tlist, _float_type(ψ0))
+
+ L_evo = _mesolve_make_L_QobjEvo(H, c_ops) + _mesolve_make_L_QobjEvo(nothing, sc_ops_list)
+ check_dimensions(L_evo, ψ0)
+ dims = L_evo.dimensions
+
+ T = Base.promote_eltype(L_evo, ψ0)
+ ρ0 = if isoperket(ψ0) # Convert it to dense vector with complex element type
+ to_dense(_complex_float_type(T), copy(ψ0.data))
+ else
+ to_dense(_complex_float_type(T), mat2vec(ket2dm(ψ0).data))
+ end
+
+ progr = ProgressBar(length(tlist), enable = getVal(progress_bar))
+
+ sc_ops_evo_data = Tuple(map(get_data ∘ QobjEvo, sc_ops_list))
+
+ K = get_data(L_evo)
+
+ Id = I(prod(dims))
+ Id_op = IdentityOperator(prod(dims)^2)
+ D_l = map(sc_ops_evo_data) do op
+ # TODO: # Currently, we are assuming a time-independent MatrixOperator
+ # Also, the u state may become non-hermitian, so Tr[Sn * ρ + ρ * Sn'] != real(Tr[Sn * ρ]) / 2
+ op_vec = mat2vec(adjoint(op.A))
+ return AddedOperator(_spre(op, Id), _spost(op', Id), _smesolve_ScalarOperator(op_vec) * Id_op)
+ end
+ D = DiffusionOperator(D_l)
+
+ kwargs2 = _merge_saveat(tlist, e_ops, DEFAULT_SDE_SOLVER_OPTIONS; kwargs...)
+ kwargs3 = _generate_stochastic_kwargs(
+ e_ops,
+ sc_ops_list,
+ makeVal(progress_bar),
+ tlist,
+ makeVal(store_measurement),
+ kwargs2,
+ SaveFuncSMESolve,
+ )
+
+ tspan = (tlist[1], tlist[end])
+ noise = _make_noise(tspan[1], sc_ops, makeVal(store_measurement), rng)
+ noise_rate_prototype = sc_ops_isa_Qobj ? nothing : similar(ρ0, length(ρ0), length(sc_ops_list))
+ prob = SDEProblem{true}(
+ K,
+ D,
+ ρ0,
+ tspan,
+ params;
+ noise_rate_prototype = noise_rate_prototype,
+ noise = noise,
+ kwargs3...,
+ )
+
+ return TimeEvolutionProblem(prob, tlist, dims, (isoperket = Val(isoperket(ψ0)),))
+end
+
+@doc raw"""
+ smesolveEnsembleProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject,
+ tlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ prob_func::Union{Function, Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ progress_bar::Union{Val,Bool} = Val(true),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+ )
+
+Generate the SDEProblem for the Stochastic Master Equation time evolution of an open quantum system. This is defined by the following stochastic differential equation:
+
+```math
+d \rho (t) = -i [\hat{H}, \rho(t)] dt + \sum_i \mathcal{D}[\hat{C}_i] \rho(t) dt + \sum_n \mathcal{D}[\hat{S}_n] \rho(t) dt + \sum_n \mathcal{H}[\hat{S}_n] \rho(t) dW_n(t),
+```
+
+where
+
+```math
+\mathcal{D}[\hat{O}] \rho = \hat{O} \rho \hat{O}^\dagger - \frac{1}{2} \{\hat{O}^\dagger \hat{O}, \rho\},
+```
+
+is the Lindblad superoperator, and
+
+```math
+\mathcal{H}[\hat{O}] \rho = \hat{O} \rho + \rho \hat{O}^\dagger - \mathrm{Tr}[\hat{O} \rho + \rho \hat{O}^\dagger] \rho,
+```
+
+Above, ``\hat{C}_i`` represent the collapse operators related to pure dissipation, while ``\hat{S}_n`` are the stochastic collapse operators. The ``dW_n(t)`` term is the real Wiener increment associated to ``\hat{S}_n``. See [Wiseman2009Quantum](@cite) for more details.
+
+# Arguments
+
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``. It can be either a [`Ket`](@ref), [`Operator`](@ref) or [`OperatorKet`](@ref).
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `c_ops`: List of collapse operators ``\{\hat{C}_i\}_i``. It can be either a `Vector` or a `Tuple`.
+- `sc_ops`: List of stochastic collapse operators ``\{\hat{S}_n\}_n``. It can be either a `Vector`, a `Tuple` or a [`AbstractQuantumObject`](@ref). It is recommended to use the last case when only one operator is provided.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: `NullParameters` of parameters to pass to the solver.
+- `rng`: Random number generator for reproducibility.
+- `ntraj`: Number of trajectories to use. Default is `500`.
+- `ensemblealg`: Ensemble method to use. Default to `EnsembleThreads()`.
+- `prob_func`: Function to use for generating the SDEProblem.
+- `output_func`: a `Tuple` containing the `Function` to use for generating the output of a single trajectory, the (optional) `ProgressBar` object, and the (optional) `RemoteChannel` object.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `store_measurement`: Whether to store the measurement expectation values. Default is `Val(false)`.
+- `kwargs`: The keyword arguments for the ODEProblem.
+
+# Notes
+
+- The states will be saved depend on the keyword argument `saveat` in `kwargs`.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). You can also specify `e_ops` and `saveat` separately.
+- The default tolerances in `kwargs` are given as `reltol=1e-2` and `abstol=1e-2`.
+- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/)
+
+!!! tip "Performance Tip"
+ When `sc_ops` contains only a single operator, it is recommended to pass only that operator as the argument. This ensures that the stochastic noise is diagonal, making the simulation faster.
+
+# Returns
+
+- `prob`: The [`TimeEvolutionProblem`](@ref) containing the Ensemble `SDEProblem` for the Stochastic Master Equation time evolution.
+"""
+function smesolveEnsembleProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{StateOpType},
+ tlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ prob_func::Union{Function,Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ progress_bar::Union{Val,Bool} = Val(true),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+) where {StateOpType<:Union{Ket,Operator,OperatorKet}}
+ _prob_func =
+ isnothing(prob_func) ?
+ _ensemble_dispatch_prob_func(
+ rng,
+ ntraj,
+ tlist,
+ _stochastic_prob_func;
+ sc_ops = sc_ops,
+ store_measurement = makeVal(store_measurement),
+ ) : prob_func
+ _output_func =
+ output_func isa Nothing ?
+ _ensemble_dispatch_output_func(ensemblealg, progress_bar, ntraj, _stochastic_output_func) : output_func
+
+ prob_sme = smesolveProblem(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ sc_ops;
+ e_ops = e_ops,
+ params = params,
+ rng = rng,
+ progress_bar = Val(false),
+ store_measurement = makeVal(store_measurement),
+ kwargs...,
+ )
+
+ ensemble_prob = TimeEvolutionProblem(
+ EnsembleProblem(prob_sme, prob_func = _prob_func, output_func = _output_func[1], safetycopy = true),
+ prob_sme.times,
+ prob_sme.dimensions,
+ merge(prob_sme.kwargs, (progr = _output_func[2], channel = _output_func[3])),
+ )
+
+ return ensemble_prob
+end
+
+@doc raw"""
+ smesolve(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject,
+ tlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ alg::Union{Nothing,StochasticDiffEqAlgorithm} = nothing,
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ prob_func::Union{Function, Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ progress_bar::Union{Val,Bool} = Val(true),
+ keep_runs_results::Union{Val,Bool} = Val(false),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+ )
+
+Stochastic Master Equation time evolution of an open quantum system. This is defined by the following stochastic differential equation:
+
+```math
+d \rho (t) = -i [\hat{H}, \rho(t)] dt + \sum_i \mathcal{D}[\hat{C}_i] \rho(t) dt + \sum_n \mathcal{D}[\hat{S}_n] \rho(t) dt + \sum_n \mathcal{H}[\hat{S}_n] \rho(t) dW_n(t),
+```
+
+where
+
+```math
+\mathcal{D}[\hat{O}] \rho = \hat{O} \rho \hat{O}^\dagger - \frac{1}{2} \{\hat{O}^\dagger \hat{O}, \rho\},
+```
+
+is the Lindblad superoperator, and
+
+```math
+\mathcal{H}[\hat{O}] \rho = \hat{O} \rho + \rho \hat{O}^\dagger - \mathrm{Tr}[\hat{O} \rho + \rho \hat{O}^\dagger] \rho,
+```
+
+Above, ``\hat{C}_i`` represent the collapse operators related to pure dissipation, while ``\hat{S}_n`` are the stochastic co operators. The ``dW_n(t)`` term is the real Wiener increment associated to ``\hat{S}_n``. See [Wiseman2009Quantum](@cite) for more details.
+
+# Arguments
+
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``. It can be either a [`Ket`](@ref), [`Operator`](@ref) or [`OperatorKet`](@ref).
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `c_ops`: List of collapse operators ``\{\hat{C}_i\}_i``. It can be either a `Vector` or a `Tuple`.
+- `sc_ops`: List of stochastic collapse operators ``\{\hat{S}_n\}_n``. It can be either a `Vector`, a `Tuple` or a [`AbstractQuantumObject`](@ref). It is recommended to use the last case when only one operator is provided.
+- `alg`: The algorithm to use for the stochastic differential equation. Default is `SRIW1()` if `sc_ops` is an [`AbstractQuantumObject`](@ref) (diagonal noise), and `SRA2()` otherwise (non-diagonal noise).
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: `NullParameters` of parameters to pass to the solver.
+- `rng`: Random number generator for reproducibility.
+- `ntraj`: Number of trajectories to use. Default is `500`.
+- `ensemblealg`: Ensemble method to use. Default to `EnsembleThreads()`.
+- `prob_func`: Function to use for generating the SDEProblem.
+- `output_func`: a `Tuple` containing the `Function` to use for generating the output of a single trajectory, the (optional) `ProgressBar` object, and the (optional) `RemoteChannel` object.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `keep_runs_results`: Whether to save the results of each trajectory. Default to `Val(false)`.
+- `store_measurement`: Whether to store the measurement expectation values. Default is `Val(false)`.
+- `kwargs`: The keyword arguments for the ODEProblem.
+
+# Notes
+
+- The states will be saved depend on the keyword argument `saveat` in `kwargs`.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). You can also specify `e_ops` and `saveat` separately.
+- The default tolerances in `kwargs` are given as `reltol=1e-2` and `abstol=1e-2`.
+- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/)
+
+!!! tip "Performance Tip"
+ When `sc_ops` contains only a single operator, it is recommended to pass only that operator as the argument. This ensures that the stochastic noise is diagonal, making the simulation faster.
+
+# Returns
+
+- `sol::TimeEvolutionStochasticSol`: The solution of the time evolution. See [`TimeEvolutionStochasticSol`](@ref).
+"""
+function smesolve(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{StateOpType},
+ tlist::AbstractVector,
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ alg::Union{Nothing,StochasticDiffEqAlgorithm} = nothing,
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ prob_func::Union{Function,Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ progress_bar::Union{Val,Bool} = Val(true),
+ keep_runs_results::Union{Val,Bool} = Val(false),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+) where {StateOpType<:Union{Ket,Operator,OperatorKet}}
+ ensemble_prob = smesolveEnsembleProblem(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ sc_ops;
+ e_ops = e_ops,
+ params = params,
+ rng = rng,
+ ntraj = ntraj,
+ ensemblealg = ensemblealg,
+ prob_func = prob_func,
+ output_func = output_func,
+ progress_bar = progress_bar,
+ store_measurement = makeVal(store_measurement),
+ kwargs...,
+ )
+
+ sc_ops_isa_Qobj = sc_ops isa AbstractQuantumObject # We can avoid using non-diagonal noise if sc_ops is just an AbstractQuantumObject
+
+ if isnothing(alg)
+ alg = sc_ops_isa_Qobj ? SRIW1() : SRA2()
+ end
+
+ return smesolve(ensemble_prob, alg, ntraj, ensemblealg, makeVal(keep_runs_results))
+end
+
+function smesolve(
+ ens_prob::TimeEvolutionProblem,
+ alg::StochasticDiffEqAlgorithm = SRA2(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ keep_runs_results = Val(false),
+)
+ sol = _ensemble_dispatch_solve(ens_prob, alg, ensemblealg, ntraj)
+
+ _sol_1 = sol[:, 1]
+ _expvals_sol_1 = _get_expvals(_sol_1, SaveFuncMESolve)
+ _m_expvals_sol_1 = _get_m_expvals(_sol_1, SaveFuncSMESolve)
+
+ dims = ens_prob.dimensions
+ _expvals_all =
+ _expvals_sol_1 isa Nothing ? nothing : map(i -> _get_expvals(sol[:, i], SaveFuncMESolve), eachindex(sol))
+ expvals_all = _expvals_all isa Nothing ? nothing : stack(_expvals_all, dims = 2) # Stack on dimension 2 to align with QuTiP
+
+ # stack to transform Vector{Vector{QuantumObject}} -> Matrix{QuantumObject}
+ states_all = stack(
+ map(i -> _smesolve_generate_state.(sol[:, i].u, Ref(dims), ens_prob.kwargs.isoperket), eachindex(sol)),
+ dims = 1,
+ )
+
+ _m_expvals =
+ _m_expvals_sol_1 isa Nothing ? nothing : map(i -> _get_m_expvals(sol[:, i], SaveFuncSMESolve), eachindex(sol))
+ m_expvals = _m_expvals isa Nothing ? nothing : stack(_m_expvals, dims = 2) # Stack on dimension 2 to align with QuTiP
+
+ kwargs = NamedTuple(_sol_1.prob.kwargs) # Convert to NamedTuple for Zygote.jl compatibility
+
+ return TimeEvolutionStochasticSol(
+ ntraj,
+ ens_prob.times,
+ _sol_1.t,
+ _store_multitraj_states(states_all, keep_runs_results),
+ _store_multitraj_expect(expvals_all, keep_runs_results),
+ m_expvals, # Measurement expectation values
+ sol.converged,
+ _sol_1.alg,
+ kwargs.abstol,
+ kwargs.reltol,
+ )
+end
diff --git a/src/time_evolution/ssesolve.jl b/src/time_evolution/ssesolve.jl
new file mode 100644
index 000000000..99c143795
--- /dev/null
+++ b/src/time_evolution/ssesolve.jl
@@ -0,0 +1,435 @@
+export ssesolveProblem, ssesolveEnsembleProblem, ssesolve
+
+# TODO: Implement the three-argument dot function for SciMLOperators.jl
+# Currently, we are assuming a time-independent MatrixOperator
+function _ssesolve_update_coeff(u, p, t, op)
+ normalize!(u)
+ return real(dot(u, op.A, u)) #this is en/2: /2 = Re
+end
+
+_ScalarOperator_e(op, f = +) = ScalarOperator(one(eltype(op)), (a, u, p, t) -> f(_ssesolve_update_coeff(u, p, t, op)))
+
+_ScalarOperator_e2_2(op, f = +) =
+ ScalarOperator(one(eltype(op)), (a, u, p, t) -> f(_ssesolve_update_coeff(u, p, t, op)^2 / 2))
+
+@doc raw"""
+ ssesolveProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
+ tlist::AbstractVector,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ store_measurement::Union{Val, Bool} = Val(false),
+ kwargs...,
+ )
+
+Generate 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 \hat{K} |\psi(t)\rangle dt + \sum_n \hat{M}_n |\psi(t)\rangle dW_n(t)
+```
+
+where
+
+```math
+\hat{K} = \hat{H} + i \sum_n \left(\frac{e_n}{2} \hat{S}_n - \frac{1}{2} \hat{S}_n^\dagger \hat{S}_n - \frac{e_n^2}{8}\right),
+```
+```math
+\hat{M}_n = \hat{S}_n - \frac{e_n}{2},
+```
+and
+```math
+e_n = \langle \hat{S}_n + \hat{S}_n^\dagger \rangle.
+```
+
+Above, ``\hat{S}_n`` are the stochastic collapse operators and ``dW_n(t)`` is the real Wiener increment associated to ``\hat{S}_n``. See [Wiseman2009Quantum](@cite) for more details.
+
+# Arguments
+
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``.
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `sc_ops`: List of stochastic collapse operators ``\{\hat{S}_n\}_n``. It can be either a `Vector`, a `Tuple` or a [`AbstractQuantumObject`](@ref). It is recommended to use the last case when only one operator is provided.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: `NullParameters` of parameters to pass to the solver.
+- `rng`: Random number generator for reproducibility.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `store_measurement`: Whether to store the measurement results. Default is `Val(false)`.
+- `kwargs`: The keyword arguments for the ODEProblem.
+
+# Notes
+
+- The states will be saved depend on the keyword argument `saveat` in `kwargs`.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). You can also specify `e_ops` and `saveat` separately.
+- The default tolerances in `kwargs` are given as `reltol=1e-2` and `abstol=1e-2`.
+- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/)
+
+!!! tip "Performance Tip"
+ When `sc_ops` contains only a single operator, it is recommended to pass only that operator as the argument. This ensures that the stochastic noise is diagonal, making the simulation faster.
+
+# Returns
+
+- `prob`: The `SDEProblem` for the Stochastic Schrödinger time evolution of the system.
+"""
+function ssesolveProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
+ tlist::AbstractVector,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ progress_bar::Union{Val,Bool} = Val(true),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+)
+ haskey(kwargs, :save_idxs) &&
+ throw(ArgumentError("The keyword argument \"save_idxs\" is not supported in QuantumToolbox."))
+
+ sc_ops isa Nothing &&
+ throw(ArgumentError("The list of stochastic collapse operators must be provided. Use sesolveProblem instead."))
+ sc_ops_list = _make_c_ops_list(sc_ops) # If it is an AbstractQuantumObject but we need to iterate
+ sc_ops_isa_Qobj = sc_ops isa AbstractQuantumObject # We can avoid using non-diagonal noise if sc_ops is just an AbstractQuantumObject
+
+ tlist = _check_tlist(tlist, _float_type(ψ0))
+
+ H_eff_evo = _mcsolve_make_Heff_QobjEvo(H, sc_ops_list)
+ isoper(H_eff_evo) || throw(ArgumentError("The Hamiltonian must be an Operator."))
+ check_dimensions(H_eff_evo, ψ0)
+ dims = H_eff_evo.dimensions
+
+ ψ0 = to_dense(_complex_float_type(ψ0), get_data(ψ0))
+
+ progr = ProgressBar(length(tlist), enable = getVal(progress_bar))
+
+ sc_ops_evo_data = Tuple(map(get_data ∘ QobjEvo, sc_ops_list))
+
+ # Here the coefficients depend on the state, so this is a non-linear operator, which should be implemented with FunctionOperator instead. However, the nonlinearity is only on the coefficients, and it should be safe.
+ K_l = sum(
+ op -> _ScalarOperator_e(op, +) * op + _ScalarOperator_e2_2(op, -) * IdentityOperator(prod(dims)),
+ sc_ops_evo_data,
+ )
+
+ K = get_data(-1im * QuantumObjectEvolution(H_eff_evo)) + K_l
+
+ D_l = map(op -> op + _ScalarOperator_e(op, -) * IdentityOperator(prod(dims)), sc_ops_evo_data)
+ D = DiffusionOperator(D_l)
+
+ kwargs2 = _merge_saveat(tlist, e_ops, DEFAULT_SDE_SOLVER_OPTIONS; kwargs...)
+ kwargs3 = _generate_stochastic_kwargs(
+ e_ops,
+ sc_ops_list,
+ makeVal(progress_bar),
+ tlist,
+ makeVal(store_measurement),
+ kwargs2,
+ SaveFuncSSESolve,
+ )
+
+ tspan = (tlist[1], tlist[end])
+ noise = _make_noise(tspan[1], sc_ops, makeVal(store_measurement), rng)
+ noise_rate_prototype = sc_ops_isa_Qobj ? nothing : similar(ψ0, length(ψ0), length(sc_ops_list))
+ prob = SDEProblem{true}(
+ K,
+ D,
+ ψ0,
+ tspan,
+ params;
+ noise_rate_prototype = noise_rate_prototype,
+ noise = noise,
+ kwargs3...,
+ )
+
+ return TimeEvolutionProblem(prob, tlist, dims)
+end
+
+@doc raw"""
+ ssesolveEnsembleProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
+ tlist::AbstractVector,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ prob_func::Union{Function, Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ progress_bar::Union{Val,Bool} = Val(true),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+ )
+
+Generate 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 \hat{K} |\psi(t)\rangle dt + \sum_n \hat{M}_n |\psi(t)\rangle dW_n(t)
+```
+
+where
+
+```math
+\hat{K} = \hat{H} + i \sum_n \left(\frac{e_n}{2} \hat{S}_n - \frac{1}{2} \hat{S}_n^\dagger \hat{S}_n - \frac{e_n^2}{8}\right),
+```
+```math
+\hat{M}_n = \hat{S}_n - \frac{e_n}{2},
+```
+and
+```math
+e_n = \langle \hat{S}_n + \hat{S}_n^\dagger \rangle.
+```
+
+Above, ``\hat{S}_n`` are the stochastic collapse operators and ``dW_n(t)`` is the real Wiener increment associated to ``\hat{S}_n``. See [Wiseman2009Quantum](@cite) for more details.
+
+# Arguments
+
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``.
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `sc_ops`: List of stochastic collapse operators ``\{\hat{S}_n\}_n``. It can be either a `Vector`, a `Tuple` or a [`AbstractQuantumObject`](@ref). It is recommended to use the last case when only one operator is provided.
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: `NullParameters` of parameters to pass to the solver.
+- `rng`: Random number generator for reproducibility.
+- `ntraj`: Number of trajectories to use. Default is `500`.
+- `ensemblealg`: Ensemble method to use. Default to `EnsembleThreads()`.
+- `jump_callback`: The Jump Callback type: Discrete or Continuous. The default is `ContinuousLindbladJumpCallback()`, which is more precise.
+- `prob_func`: Function to use for generating the SDEProblem.
+- `output_func`: a `Tuple` containing the `Function` to use for generating the output of a single trajectory, the (optional) `ProgressBar` object, and the (optional) `RemoteChannel` object.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `store_measurement`: Whether to store the measurement results. Default is `Val(false)`.
+- `kwargs`: The keyword arguments for the ODEProblem.
+
+# Notes
+
+- The states will be saved depend on the keyword argument `saveat` in `kwargs`.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). You can also specify `e_ops` and `saveat` separately.
+- The default tolerances in `kwargs` are given as `reltol=1e-2` and `abstol=1e-2`.
+- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/)
+
+!!! tip "Performance Tip"
+ When `sc_ops` contains only a single operator, it is recommended to pass only that operator as the argument. This ensures that the stochastic noise is diagonal, making the simulation faster.
+
+# Returns
+
+- `prob::EnsembleProblem with SDEProblem`: The Ensemble SDEProblem for the Stochastic Shrödinger time evolution.
+"""
+function ssesolveEnsembleProblem(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
+ tlist::AbstractVector,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ prob_func::Union{Function,Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ progress_bar::Union{Val,Bool} = Val(true),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+)
+ _prob_func =
+ isnothing(prob_func) ?
+ _ensemble_dispatch_prob_func(
+ rng,
+ ntraj,
+ tlist,
+ _stochastic_prob_func;
+ sc_ops = sc_ops,
+ store_measurement = makeVal(store_measurement),
+ ) : prob_func
+ _output_func =
+ output_func isa Nothing ?
+ _ensemble_dispatch_output_func(ensemblealg, progress_bar, ntraj, _stochastic_output_func) : output_func
+
+ prob_sme = ssesolveProblem(
+ H,
+ ψ0,
+ tlist,
+ sc_ops;
+ e_ops = e_ops,
+ params = params,
+ rng = rng,
+ progress_bar = Val(false),
+ store_measurement = makeVal(store_measurement),
+ kwargs...,
+ )
+
+ ensemble_prob = TimeEvolutionProblem(
+ EnsembleProblem(prob_sme, prob_func = _prob_func, output_func = _output_func[1], safetycopy = true),
+ prob_sme.times,
+ prob_sme.dimensions,
+ (progr = _output_func[2], channel = _output_func[3]),
+ )
+
+ return ensemble_prob
+end
+
+@doc raw"""
+ ssesolve(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
+ tlist::AbstractVector,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ alg::Union{Nothing,StochasticDiffEqAlgorithm} = nothing,
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ prob_func::Union{Function, Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ progress_bar::Union{Val,Bool} = Val(true),
+ keep_runs_results::Union{Val,Bool} = Val(false),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+ )
+
+
+Stochastic Schrödinger equation evolution of a quantum system given the system Hamiltonian ``\hat{H}`` and a list of stochastic collapse (jump) operators ``\{\hat{S}_n\}_n``.
+The stochastic evolution of the state ``|\psi(t)\rangle`` is defined by:
+
+```math
+d|\psi(t)\rangle = -i \hat{K} |\psi(t)\rangle dt + \sum_n \hat{M}_n |\psi(t)\rangle dW_n(t)
+```
+
+where
+
+```math
+\hat{K} = \hat{H} + i \sum_n \left(\frac{e_n}{2} \hat{S}_n - \frac{1}{2} \hat{S}_n^\dagger \hat{S}_n - \frac{e_n^2}{8}\right),
+```
+```math
+\hat{M}_n = \hat{S}_n - \frac{e_n}{2},
+```
+and
+```math
+e_n = \langle \hat{S}_n + \hat{S}_n^\dagger \rangle.
+```
+
+Above, ``\hat{S}_n`` are the stochastic collapse operators and ``dW_n(t)`` is the real Wiener increment associated to ``\hat{S}_n``. See [Wiseman2009Quantum](@cite) for more details.
+
+
+# Arguments
+
+- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
+- `ψ0`: Initial state of the system ``|\psi(0)\rangle``.
+- `tlist`: List of time points at which to save either the state or the expectation values of the system.
+- `sc_ops`: List of stochastic collapse operators ``\{\hat{S}_n\}_n``. It can be either a `Vector`, a `Tuple` or a [`AbstractQuantumObject`](@ref). It is recommended to use the last case when only one operator is provided.
+- `alg`: The algorithm to use for the stochastic differential equation. Default is `SRIW1()` if `sc_ops` is an [`AbstractQuantumObject`](@ref) (diagonal noise), and `SRA2()` otherwise (non-diagonal noise).
+- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
+- `params`: `NullParameters` of parameters to pass to the solver.
+- `rng`: Random number generator for reproducibility.
+- `ntraj`: Number of trajectories to use. Default is `500`.
+- `ensemblealg`: Ensemble method to use. Default to `EnsembleThreads()`.
+- `prob_func`: Function to use for generating the SDEProblem.
+- `output_func`: a `Tuple` containing the `Function` to use for generating the output of a single trajectory, the (optional) `ProgressBar` object, and the (optional) `RemoteChannel` object.
+- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
+- `keep_runs_results`: Whether to save the results of each trajectory. Default to `Val(false)`.
+- `store_measurement`: Whether to store the measurement results. Default is `Val(false)`.
+- `kwargs`: The keyword arguments for the ODEProblem.
+
+# Notes
+
+- The states will be saved depend on the keyword argument `saveat` in `kwargs`.
+- If `e_ops` is empty, the default value of `saveat=tlist` (saving the states corresponding to `tlist`), otherwise, `saveat=[tlist[end]]` (only save the final state). You can also specify `e_ops` and `saveat` separately.
+- The default tolerances in `kwargs` are given as `reltol=1e-2` and `abstol=1e-2`.
+- For more details about `alg` please refer to [`DifferentialEquations.jl` (SDE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/sde_solve/)
+- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/)
+
+!!! tip "Performance Tip"
+ When `sc_ops` contains only a single operator, it is recommended to pass only that operator as the argument. This ensures that the stochastic noise is diagonal, making the simulation faster.
+
+# Returns
+
+- `sol::TimeEvolutionStochasticSol`: The solution of the time evolution. See [`TimeEvolutionStochasticSol`](@ref).
+"""
+function ssesolve(
+ H::Union{AbstractQuantumObject{Operator},Tuple},
+ ψ0::QuantumObject{Ket},
+ tlist::AbstractVector,
+ sc_ops::Union{Nothing,AbstractVector,Tuple,AbstractQuantumObject} = nothing;
+ alg::Union{Nothing,StochasticDiffEqAlgorithm} = nothing,
+ e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
+ params = NullParameters(),
+ rng::AbstractRNG = default_rng(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ prob_func::Union{Function,Nothing} = nothing,
+ output_func::Union{Tuple,Nothing} = nothing,
+ progress_bar::Union{Val,Bool} = Val(true),
+ keep_runs_results::Union{Val,Bool} = Val(false),
+ store_measurement::Union{Val,Bool} = Val(false),
+ kwargs...,
+)
+ ens_prob = ssesolveEnsembleProblem(
+ H,
+ ψ0,
+ tlist,
+ sc_ops;
+ e_ops = e_ops,
+ params = params,
+ rng = rng,
+ ntraj = ntraj,
+ ensemblealg = ensemblealg,
+ prob_func = prob_func,
+ output_func = output_func,
+ progress_bar = progress_bar,
+ store_measurement = makeVal(store_measurement),
+ kwargs...,
+ )
+
+ sc_ops_isa_Qobj = sc_ops isa AbstractQuantumObject # We can avoid using non-diagonal noise if sc_ops is just an AbstractQuantumObject
+
+ if isnothing(alg)
+ alg = sc_ops_isa_Qobj ? SRIW1() : SRA2()
+ end
+
+ return ssesolve(ens_prob, alg, ntraj, ensemblealg, makeVal(keep_runs_results))
+end
+
+function ssesolve(
+ ens_prob::TimeEvolutionProblem,
+ alg::StochasticDiffEqAlgorithm = SRA2(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
+ keep_runs_results = Val(false),
+)
+ sol = _ensemble_dispatch_solve(ens_prob, alg, ensemblealg, ntraj)
+
+ _sol_1 = sol[:, 1]
+ _expvals_sol_1 = _get_expvals(_sol_1, SaveFuncSSESolve)
+ _m_expvals_sol_1 = _get_m_expvals(_sol_1, SaveFuncSSESolve)
+
+ normalize_states = Val(false)
+ dims = ens_prob.dimensions
+ _expvals_all =
+ _expvals_sol_1 isa Nothing ? nothing : map(i -> _get_expvals(sol[:, i], SaveFuncSSESolve), eachindex(sol))
+ expvals_all = _expvals_all isa Nothing ? nothing : stack(_expvals_all, dims = 2) # Stack on dimension 2 to align with QuTiP
+
+ # stack to transform Vector{Vector{QuantumObject}} -> Matrix{QuantumObject}
+ states_all = stack(map(i -> _normalize_state!.(sol[:, i].u, Ref(dims), normalize_states), eachindex(sol)), dims = 1)
+
+ _m_expvals =
+ _m_expvals_sol_1 isa Nothing ? nothing : map(i -> _get_m_expvals(sol[:, i], SaveFuncSSESolve), eachindex(sol))
+ m_expvals = _m_expvals isa Nothing ? nothing : stack(_m_expvals, dims = 2)
+
+ kwargs = NamedTuple(_sol_1.prob.kwargs) # Convert to NamedTuple for Zygote.jl compatibility
+
+ return TimeEvolutionStochasticSol(
+ ntraj,
+ ens_prob.times,
+ _sol_1.t,
+ _store_multitraj_states(states_all, keep_runs_results),
+ _store_multitraj_expect(expvals_all, keep_runs_results),
+ m_expvals, # Measurement expectation values
+ sol.converged,
+ _sol_1.alg,
+ kwargs.abstol,
+ kwargs.reltol,
+ )
+end
diff --git a/src/time_evolution/time_evolution.jl b/src/time_evolution/time_evolution.jl
index 6751a3da1..3b09bd443 100644
--- a/src/time_evolution/time_evolution.jl
+++ b/src/time_evolution/time_evolution.jl
@@ -1,9 +1,47 @@
-export TimeDependentOperatorSum
-export TimeEvolutionSol, TimeEvolutionMCSol
+export TimeEvolutionSol
+export TimeEvolutionMultiTrajSol, TimeEvolutionMCSol, TimeEvolutionStochasticSol
+export average_states, average_expect, std_expect
-export liouvillian, liouvillian_floquet, liouvillian_generalized
+export 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-3, reltol = 2e-3, save_everystep = false, save_end = true)
+const COL_TIMES_WHICH_INIT_SIZE = 200
+
+abstract type TimeEvolutionMultiTrajSol{Tstates,Texpect} end
+
+@doc raw"""
+ struct TimeEvolutionProblem
+
+A Julia constructor for handling the `ODEProblem` of the time evolution of quantum systems.
+
+# Fields (Attributes)
+
+- `prob::AbstractSciMLProblem`: The `ODEProblem` of the time evolution.
+- `times::AbstractVector`: The time list of the evolution.
+- `dimensions::AbstractDimensions`: The dimensions of the Hilbert space.
+- `kwargs::KWT`: Generic keyword arguments.
+
+!!! note "`dims` property"
+ For a given `prob::TimeEvolutionProblem`, `prob.dims` or `getproperty(prob, :dims)` returns its `dimensions` in the type of integer-vector.
+"""
+struct TimeEvolutionProblem{PT<:AbstractSciMLProblem,TT<:AbstractVector,DT<:AbstractDimensions,KWT}
+ prob::PT
+ times::TT
+ dimensions::DT
+ kwargs::KWT
+end
+
+function Base.getproperty(prob::TimeEvolutionProblem, key::Symbol)
+ # a comment here to avoid bad render by JuliaFormatter
+ if key === :dims
+ return dimensions_to_dims(getfield(prob, :dimensions))
+ else
+ return getfield(prob, key)
+ end
+end
+
+TimeEvolutionProblem(prob, times, dims) = TimeEvolutionProblem(prob, times, dims, nothing)
@doc raw"""
struct TimeEvolutionSol
@@ -12,22 +50,33 @@ A structure storing the results and some information from solving time evolution
# Fields (Attributes)
-- `times::AbstractVector`: The time list of the evolution.
-- `states::Vector{QuantumObject}`: The list of result states.
-- `expect::Matrix`: The expectation values corresponding to each time point in `times`.
+- `times::AbstractVector`: The list of time points at which the expectation values are calculated during the evolution.
+- `times_states::AbstractVector`: The list of time points at which the states are stored during the evolution.
+- `states::Vector{QuantumObject}`: The list of result states corresponding to each time point in `times_states`.
+- `expect::Union{AbstractMatrix,Nothing}`: The expectation values corresponding to each time point in `times`.
- `retcode`: The return code from the solver.
- `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 TimeEvolutionSol{TT<:Vector{<:Real},TS<:AbstractVector,TE<:Matrix{ComplexF64}}
- times::TT
+struct TimeEvolutionSol{
+ TT1<:AbstractVector{<:Real},
+ TT2<:AbstractVector{<:Real},
+ TS<:AbstractVector,
+ TE<:Union{AbstractMatrix,Nothing},
+ RETT<:Enum,
+ AlgT<:OrdinaryDiffEqAlgorithm,
+ AT<:Real,
+ RT<:Real,
+}
+ times::TT1
+ times_states::TT2
states::TS
expect::TE
- retcode::Enum
- alg::OrdinaryDiffEqAlgorithm
- abstol::Real
- reltol::Real
+ retcode::RETT
+ alg::AlgT
+ abstol::AT
+ reltol::RT
end
function Base.show(io::IO, sol::TimeEvolutionSol)
@@ -35,7 +84,11 @@ function Base.show(io::IO, sol::TimeEvolutionSol)
print(io, "(return code: $(sol.retcode))\n")
print(io, "--------------------------\n")
print(io, "num_states = $(length(sol.states))\n")
- print(io, "num_expect = $(size(sol.expect, 1))\n")
+ if sol.expect isa Nothing
+ print(io, "num_expect = 0\n")
+ else
+ print(io, "num_expect = $(size(sol.expect, 1))\n")
+ end
print(io, "ODE alg.: $(sol.alg)\n")
print(io, "abstol = $(sol.abstol)\n")
print(io, "reltol = $(sol.reltol)\n")
@@ -43,58 +96,218 @@ function Base.show(io::IO, sol::TimeEvolutionSol)
end
@doc raw"""
- struct TimeEvolutionMCSol
+ struct TimeEvolutionMCSol <: TimeEvolutionMultiTrajSol
A structure storing the results and some information from solving quantum trajectories of the Monte Carlo wave function 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`
-- `jump_times::Vector{Vector{Real}}`: The time records of every quantum jump occurred in each trajectory.
-- `jump_which::Vector{Vector{Int}}`: The indices of the jump operators in `c_ops` that describe the corresponding quantum jumps occurred in each trajectory.
+- `ntraj::Int`: Number of trajectories
+- `times::AbstractVector`: The list of time points at which the expectation values are calculated during the evolution.
+- `times_states::AbstractVector`: The list of time points at which the states are stored during the evolution.
+- `states::AbstractVecOrMat`: The list of result states in each trajectory and each time point in `times_states`.
+- `expect::Union{AbstractArray,Nothing}`: The expectation values corresponding to each trajectory and each time point in `times`.
+- `col_times::Vector{Vector{Real}}`: The time records of every quantum jump occurred in each trajectory.
+- `col_which::Vector{Vector{Int}}`: The indices of which collapse operator was responsible for each quantum jump in `col_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.
+
+# Notes
+
+When the keyword argument `keep_runs_results` is passed as `Val(false)` to a multi-trajectory solver, the `states` and `expect` fields store only the average results (averaged over all trajectories). The results can be accessed by the following index-order:
+
+- `sol.states[time_idx]`
+- `sol.expect[e_op,time_idx]`
+
+If the keyword argument `keep_runs_results = Val(true)`, the results for each trajectory and each time are stored, and the index-order of the elements in fields `states` and `expect` are:
+
+- `sol.states[trajectory,time_idx]`
+- `sol.expect[e_op,trajectory,time_idx]`
+
+We also provide the following functions for statistical analysis of multi-trajectory solutions:
+
+- [`average_states`](@ref)
+- [`average_expect`](@ref)
+- [`std_expect`](@ref)
"""
struct TimeEvolutionMCSol{
- TT<:Vector{<:Vector{<:Real}},
- TS<:AbstractVector,
- TE<:Matrix{ComplexF64},
- TEA<:Array{ComplexF64,3},
+ TT1<:AbstractVector{<:Real},
+ TT2<:AbstractVector{<:Real},
+ TS<:AbstractVecOrMat,
+ TE<:Union{AbstractArray,Nothing},
TJT<:Vector{<:Vector{<:Real}},
TJW<:Vector{<:Vector{<:Integer}},
-}
- n_traj::Int
- times::TT
+ AlgT<:OrdinaryDiffEqAlgorithm,
+ AT<:Real,
+ RT<:Real,
+} <: TimeEvolutionMultiTrajSol{TS,TE}
+ ntraj::Int
+ times::TT1
+ times_states::TT2
states::TS
expect::TE
- expect_all::TEA
- jump_times::TJT
- jump_which::TJW
+ col_times::TJT
+ col_which::TJW
converged::Bool
- alg::OrdinaryDiffEqAlgorithm
- abstol::Real
- reltol::Real
+ alg::AlgT
+ abstol::AT
+ reltol::RT
end
function Base.show(io::IO, sol::TimeEvolutionMCSol)
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, "num_trajectories = $(sol.ntraj)\n")
+ print(io, "num_states = $(size(sol.states, ndims(sol.states)))\n") # get the size of last dimension
+ if sol.expect isa Nothing
+ print(io, "num_expect = 0\n")
+ else
+ print(io, "num_expect = $(size(sol.expect, 1))\n")
+ end
print(io, "ODE alg.: $(sol.alg)\n")
print(io, "abstol = $(sol.abstol)\n")
print(io, "reltol = $(sol.reltol)\n")
return nothing
end
+@doc raw"""
+ struct TimeEvolutionStochasticSol
+
+A structure storing the results and some information from solving trajectories of the Stochastic time evolution.
+
+# Fields (Attributes)
+
+- `ntraj::Int`: Number of trajectories
+- `times::AbstractVector`: The list of time points at which the expectation values are calculated during the evolution.
+- `times_states::AbstractVector`: The list of time points at which the states are stored during the evolution.
+- `states::AbstractVecOrMat`: The list of result states in each trajectory and each time point in `times_states`.
+- `expect::Union{AbstractArray,Nothing}`: 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.
+
+# Notes
+
+When the keyword argument `keep_runs_results` is passed as `Val(false)` to a multi-trajectory solver, the `states` and `expect` fields store only the average results (averaged over all trajectories). The results can be accessed by the following index-order:
+
+- `sol.states[time_idx]`
+- `sol.expect[e_op,time_idx]`
+
+If the keyword argument `keep_runs_results = Val(true)`, the results for each trajectory and each time are stored, and the index-order of the elements in fields `states` and `expect` are:
+
+- `sol.states[trajectory,time_idx]`
+- `sol.expect[e_op,trajectory,time_idx]`
+
+We also provide the following functions for statistical analysis of multi-trajectory solutions:
+
+- [`average_states`](@ref)
+- [`average_expect`](@ref)
+- [`std_expect`](@ref)
+"""
+struct TimeEvolutionStochasticSol{
+ TT1<:AbstractVector{<:Real},
+ TT2<:AbstractVector{<:Real},
+ TS<:AbstractVecOrMat,
+ TE<:Union{AbstractArray,Nothing},
+ TEM<:Union{AbstractArray,Nothing},
+ AlgT<:StochasticDiffEqAlgorithm,
+ AT<:Real,
+ RT<:Real,
+} <: TimeEvolutionMultiTrajSol{TS,TE}
+ ntraj::Int
+ times::TT1
+ times_states::TT2
+ states::TS
+ expect::TE
+ measurement::TEM
+ converged::Bool
+ alg::AlgT
+ abstol::AT
+ reltol::RT
+end
+
+function Base.show(io::IO, sol::TimeEvolutionStochasticSol)
+ print(io, "Solution of stochastic quantum trajectories\n")
+ print(io, "(converged: $(sol.converged))\n")
+ print(io, "--------------------------------\n")
+ print(io, "num_trajectories = $(sol.ntraj)\n")
+ print(io, "num_states = $(size(sol.states, ndims(sol.states)))\n") # get the size of last dimension
+ if sol.expect isa Nothing
+ print(io, "num_expect = 0\n")
+ else
+ print(io, "num_expect = $(size(sol.expect, 1))\n")
+ end
+ print(io, "SDE alg.: $(sol.alg)\n")
+ print(io, "abstol = $(sol.abstol)\n")
+ print(io, "reltol = $(sol.reltol)\n")
+ return nothing
+end
+
+@doc raw"""
+ average_states(sol::TimeEvolutionMultiTrajSol)
+
+Return the trajectory-averaged result states (as density [`Operator`](@ref)) at each time point.
+"""
+average_states(sol::TimeEvolutionMultiTrajSol{<:Matrix{<:QuantumObject}}) = _average_traj_states(sol.states)
+average_states(sol::TimeEvolutionMultiTrajSol{<:Vector{<:QuantumObject}}) = sol.states # this case should already be averaged over all trajectories
+
+# TODO: Check if broadcasting division ./ size(states, 1) is type stable
+_average_traj_states(states::Matrix{<:QuantumObject{Ket}}) =
+ map(x -> x / size(states, 1), dropdims(sum(ket2dm, states, dims = 1), dims = 1))
+_average_traj_states(states::Matrix{<:QuantumObject{ObjType}}) where {ObjType<:Union{Operator,OperatorKet}} =
+ map(x -> x / size(states, 1), dropdims(sum(states, dims = 1), dims = 1))
+
+@doc raw"""
+ average_expect(sol::TimeEvolutionMultiTrajSol)
+
+Return the trajectory-averaged expectation values at each time point.
+"""
+average_expect(sol::TimeEvolutionMultiTrajSol{TS,Array{T,3}}) where {TS,T<:Number} = _average_traj_expect(sol.expect)
+average_expect(sol::TimeEvolutionMultiTrajSol{TS,Matrix{T}}) where {TS,T<:Number} = sol.expect # this case should already be averaged over all trajectories
+average_expect(::TimeEvolutionMultiTrajSol{TS,Nothing}) where {TS} = nothing
+
+_average_traj_expect(expvals::Array{T,3}) where {T<:Number} =
+ dropdims(sum(expvals, dims = 2), dims = 2) ./ size(expvals, 2)
+
+# these are used in multi-trajectory solvers before returning solutions
+_store_multitraj_states(states::Matrix{<:QuantumObject}, keep_runs_results::Val{false}) = _average_traj_states(states)
+_store_multitraj_states(states::Matrix{<:QuantumObject}, keep_runs_results::Val{true}) = states
+_store_multitraj_expect(expvals::Array{T,3}, keep_runs_results::Val{false}) where {T<:Number} =
+ _average_traj_expect(expvals)
+_store_multitraj_expect(expvals::Array{T,3}, keep_runs_results::Val{true}) where {T<:Number} = expvals
+_store_multitraj_expect(expvals::Nothing, keep_runs_results) = nothing
+
+@doc raw"""
+ std_expect(sol::TimeEvolutionMultiTrajSol)
+
+Return the trajectory-wise standard deviation of the expectation values at each time point.
+"""
+function std_expect(sol::TimeEvolutionMultiTrajSol{TS,Array{T,3}}) where {TS,T<:Number}
+ # the following standard deviation (std) is defined as the square-root of variance instead of pseudo-variance
+ # i.e., it is equivalent to (even for complex expectation values):
+ # dropdims(
+ # sqrt.(mean(abs2.(sol.expect), dims = 2) .- abs2.(mean(sol.expect, dims = 2))),
+ # dims = 2
+ # )
+ # [this should be included in the runtest]
+ return dropdims(std(sol.expect, corrected = false, dims = 2), dims = 2)
+end
+std_expect(::TimeEvolutionMultiTrajSol{TS,Matrix{T}}) where {TS,T<:Number} = throw(
+ ArgumentError(
+ "Can not compute the standard deviation without the expectation values of each trajectory. Try to specify keyword argument `keep_runs_results=Val(true)` to the solver.",
+ ),
+)
+std_expect(::TimeEvolutionMultiTrajSol{TS,Nothing}) where {TS} = nothing
+
+#######################################
+#=
+ Callbacks for Monte Carlo quantum trajectories
+=#
+
abstract type LindbladJumpCallbackType end
struct ContinuousLindbladJumpCallback <: LindbladJumpCallbackType
@@ -105,111 +318,218 @@ struct DiscreteLindbladJumpCallback <: LindbladJumpCallbackType end
ContinuousLindbladJumpCallback(; interp_points::Int = 10) = ContinuousLindbladJumpCallback(interp_points)
-## Time-dependent sum of operators
+function _check_tlist(tlist, T::Type)
+ tlist2 = convert(Vector{T}, tlist) # Convert it to support GPUs and avoid type instabilities for OrdinaryDiffEq.jl
-struct TimeDependentOperatorSum{CFT,OST<:OperatorSum}
- coefficient_functions::CFT
- operator_sum::OST
+ # Check if the list of times is not empty
+ isempty(tlist2) && throw(ArgumentError("The time list must not be empty."))
+ # Check if the list of times is sorted
+ issorted(tlist2) || throw(ArgumentError("The time list must be sorted."))
+ # Check if the list of times is unique
+ allunique(tlist2) || throw(ArgumentError("The time list must be unique."))
+
+ return tlist2
end
-function TimeDependentOperatorSum(
- coefficient_functions,
- operators::Vector{<:QuantumObject};
- params = nothing,
- init_time = 0.0,
-)
- # promote the type of the coefficients and the operators. Remember that the coefficient_functions si a vector of functions and the operators is a vector of QuantumObjects
- coefficients = [f(init_time, params) for f in coefficient_functions]
- operator_sum = OperatorSum(coefficients, operators)
- return TimeDependentOperatorSum(coefficient_functions, operator_sum)
+#######################################
+
+_make_c_ops_list(c_ops) = c_ops
+_make_c_ops_list(c_ops::AbstractQuantumObject) = (c_ops,)
+
+function _merge_saveat(tlist, e_ops, default_options; kwargs...)
+ is_empty_e_ops = isnothing(e_ops) ? true : isempty(e_ops)
+ saveat = is_empty_e_ops ? tlist : [tlist[end]]
+ default_values = (default_options..., saveat = saveat)
+ kwargs2 = merge(default_values, kwargs)
+
+ # DifferentialEquations.jl has this weird save_end setting
+ # So we need to do this to make sure it's consistent
+ haskey(kwargs, :save_end) && return kwargs2
+ isempty(kwargs2.saveat) && return kwargs2
+
+ save_end = tlist[end] in kwargs2.saveat
+ return merge(kwargs2, (save_end = save_end,))
end
-Base.size(A::TimeDependentOperatorSum) = size(A.operator_sum)
-Base.size(A::TimeDependentOperatorSum, inds...) = size(A.operator_sum, inds...)
-Base.length(A::TimeDependentOperatorSum) = length(A.operator_sum)
+#######################################
+#=
+Helpers for handling output of ensemble problems.
+This is very useful especially for dispatching which method to use to update the progress bar.
+=#
+
+# Output function with progress bar update
+function _ensemble_output_func_progress(sol, i, progr, output_func)
+ next!(progr)
+ return output_func(sol, i)
+end
-function update_coefficients!(A::TimeDependentOperatorSum, t, params)
- @inbounds @simd for i in 1:length(A.coefficient_functions)
- A.operator_sum.coefficients[i] = A.coefficient_functions[i](t, params)
- end
+# Output function with distributed channel update for progress bar
+function _ensemble_output_func_distributed(sol, i, channel, output_func)
+ put!(channel, true)
+ return output_func(sol, i)
end
-(A::TimeDependentOperatorSum)(t, params) = (update_coefficients!(A, t, params); A)
+function _ensemble_dispatch_output_func(
+ ::ET,
+ progress_bar,
+ ntraj,
+ output_func,
+) where {ET<:Union{EnsembleSerial,EnsembleThreads}}
+ if getVal(progress_bar)
+ progr = ProgressBar(ntraj, enable = getVal(progress_bar))
+ f = (sol, i) -> _ensemble_output_func_progress(sol, i, progr, output_func)
+ return (f, progr, nothing)
+ else
+ return (output_func, nothing, nothing)
+ end
+end
+function _ensemble_dispatch_output_func(
+ ::ET,
+ progress_bar,
+ ntraj,
+ output_func,
+) where {ET<:Union{EnsembleSplitThreads,EnsembleDistributed}}
+ if getVal(progress_bar)
+ progr = ProgressBar(ntraj, enable = getVal(progress_bar))
+ progr_channel::RemoteChannel{Channel{Bool}} = RemoteChannel(() -> Channel{Bool}(1))
+
+ f = (sol, i) -> _ensemble_output_func_distributed(sol, i, progr_channel, output_func)
+ return (f, progr, progr_channel)
+ else
+ return (output_func, nothing, nothing)
+ end
+end
-@inline function LinearAlgebra.mul!(y::AbstractVector, A::TimeDependentOperatorSum, x::AbstractVector, α, β)
- return mul!(y, A.operator_sum, x, α, β)
+function _ensemble_dispatch_prob_func(rng, ntraj, tlist, prob_func; kwargs...)
+ seeds = map(i -> rand(rng, UInt64), 1:ntraj)
+ return (prob, i, repeat) -> prob_func(prob, i, repeat, rng, seeds, tlist; kwargs...)
end
-#######################################
+function _ensemble_dispatch_solve(
+ ens_prob_mc::TimeEvolutionProblem,
+ alg::Union{<:OrdinaryDiffEqAlgorithm,<:StochasticDiffEqAlgorithm},
+ ensemblealg::ET,
+ ntraj::Int,
+) where {ET<:Union{EnsembleSplitThreads,EnsembleDistributed}}
+ sol = nothing
+
+ @sync begin
+ @async while take!(ens_prob_mc.kwargs.channel)
+ next!(ens_prob_mc.kwargs.progr)
+ end
-### LIOUVILLIAN ###
-@doc raw"""
- liouvillian(H::QuantumObject, c_ops::Union{AbstractVector,Nothing}=nothing, Id_cache=I(prod(H.dims)))
+ @async begin
+ sol = solve(ens_prob_mc.prob, alg, ensemblealg, trajectories = ntraj)
+ put!(ens_prob_mc.kwargs.channel, false)
+ end
+ end
-Construct the Liouvillian [`SuperOperator`](@ref) for a system Hamiltonian ``\hat{H}`` and a set of collapse operators ``\{\hat{C}_n\}_n``:
+ return sol
+end
+function _ensemble_dispatch_solve(
+ ens_prob_mc::TimeEvolutionProblem,
+ alg::Union{<:OrdinaryDiffEqAlgorithm,<:StochasticDiffEqAlgorithm},
+ ensemblealg,
+ ntraj::Int,
+)
+ sol = solve(ens_prob_mc.prob, alg, ensemblealg, trajectories = ntraj)
+ return sol
+end
-```math
-\mathcal{L} [\cdot] = -i[\hat{H}, \cdot] + \sum_n \mathcal{D}(\hat{C}_n) [\cdot]
-```
+#######################################
+#=
+ Stochastic funcs
+=#
+function _stochastic_prob_func(prob, i, repeat, rng, seeds, tlist; kwargs...)
+ seed = seeds[i]
+ traj_rng = typeof(rng)()
+ seed!(traj_rng, seed)
+
+ sc_ops = kwargs[:sc_ops]
+ store_measurement = kwargs[:store_measurement]
+ noise = _make_noise(prob.prob.tspan[1], sc_ops, store_measurement, traj_rng)
+
+ return remake(prob.prob, noise = noise, seed = seed)
+end
-where
+# Standard output function
+_stochastic_output_func(sol, i) = (sol, false)
+
+#=
+ Define diagonal or non-diagonal noise depending on the type of `sc_ops`.
+ If `sc_ops` is a `AbstractQuantumObject`, we avoid using the non-diagonal noise.
+=#
+function _make_noise(t0, sc_ops, store_measurement::Val, rng)
+ noise = RealWienerProcess!(
+ t0,
+ zeros(length(sc_ops)),
+ zeros(length(sc_ops)),
+ save_everystep = getVal(store_measurement),
+ rng = rng,
+ )
+
+ return noise
+end
+function _make_noise(t0, sc_ops::AbstractQuantumObject, store_measurement::Val, rng)
+ noise = RealWienerProcess(t0, 0.0, 0.0, save_everystep = getVal(store_measurement), rng = rng)
-```math
-\mathcal{D}(\hat{C}_n) [\cdot] = \hat{C}_n [\cdot] \hat{C}_n^\dagger - \frac{1}{2} \hat{C}_n^\dagger \hat{C}_n [\cdot] - \frac{1}{2} [\cdot] \hat{C}_n^\dagger \hat{C}_n
-```
+ return noise
+end
-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.
+#=
+ struct DiffusionOperator
-See also [`spre`](@ref), [`spost`](@ref), and [`lindblad_dissipator`](@ref).
-"""
-function liouvillian(
- H::QuantumObject{MT1,OpType1},
- c_ops::Union{AbstractVector,Nothing} = nothing,
- Id_cache = I(prod(H.dims)),
-) where {MT1<:AbstractMatrix,OpType1<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}}
- L = liouvillian(H, Id_cache)
- if !(c_ops isa Nothing)
- for c_op in c_ops
- L += lindblad_dissipator(c_op, Id_cache)
- end
+A struct to represent the diffusion operator. This is used to perform the diffusion process on N different Wiener processes.
+=#
+struct DiffusionOperator{T,OpType<:Tuple{Vararg{AbstractSciMLOperator}}}
+ ops::OpType
+ function DiffusionOperator(ops::OpType) where {OpType}
+ T = mapreduce(eltype, promote_type, ops)
+ return new{T,OpType}(ops)
end
- return L
end
-liouvillian(H::QuantumObject{<:AbstractMatrix,OperatorQuantumObject}, Id_cache::Diagonal = I(prod(H.dims))) =
- -1im * (spre(H, Id_cache) - spost(H, Id_cache))
+@generated function (L::DiffusionOperator)(w, v, p, t)
+ ops_types = L.parameters[2].parameters
+ N = length(ops_types)
+ quote
+ M = length(v)
+ S = (size(w, 1), size(w, 2)) # This supports also `w` as a `Vector`
+ (S[1] == M && S[2] == $N) || throw(DimensionMismatch("The size of the output vector is incorrect."))
+ Base.@nexprs $N i -> begin
+ op = L.ops[i]
+ op(@view(w[:, i]), v, v, p, t)
+ end
+ return w
+ end
+end
-liouvillian(H::QuantumObject{<:AbstractMatrix,SuperOperatorQuantumObject}, Id_cache::Diagonal) = H
+#######################################
function liouvillian_floquet(
- L₀::QuantumObject{<:AbstractArray{T1},SuperOperatorQuantumObject},
- Lₚ::QuantumObject{<:AbstractArray{T2},SuperOperatorQuantumObject},
- Lₘ::QuantumObject{<:AbstractArray{T3},SuperOperatorQuantumObject},
+ L₀::QuantumObject{SuperOperator},
+ Lₚ::QuantumObject{SuperOperator},
+ Lₘ::QuantumObject{SuperOperator},
ω::Real;
n_max::Int = 3,
tol::Real = 1e-15,
-) where {T1,T2,T3}
- ((L₀.dims == Lₚ.dims) && (L₀.dims == Lₘ.dims)) ||
- throw(DimensionMismatch("The quantum objects are not of the same Hilbert dimension."))
-
+)
+ check_dimensions(L₀, Lₚ, Lₘ)
return _liouvillian_floquet(L₀, Lₚ, Lₘ, ω, n_max, tol)
end
function liouvillian_floquet(
- H::QuantumObject{<:AbstractArray{T1},OpType1},
- Hₚ::QuantumObject{<:AbstractArray{T2},OpType2},
- Hₘ::QuantumObject{<:AbstractArray{T3},OpType3},
+ H::QuantumObject{OpType1},
+ Hₚ::QuantumObject{OpType2},
+ Hₘ::QuantumObject{OpType3},
ω::Real,
- c_ops::Union{AbstractVector,Nothing} = nothing;
+ c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
n_max::Int = 3,
tol::Real = 1e-15,
) where {
- T1,
- T2,
- T3,
- OpType1<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- OpType2<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
- OpType3<:Union{OperatorQuantumObject,SuperOperatorQuantumObject},
+ OpType1<:Union{Operator,SuperOperator},
+ OpType2<:Union{Operator,SuperOperator},
+ OpType3<:Union{Operator,SuperOperator},
}
return liouvillian_floquet(liouvillian(H, c_ops), liouvillian(Hₚ), liouvillian(Hₘ), ω, n_max = n_max, tol = tol)
end
@@ -223,30 +543,30 @@ Constructs the generalized Liouvillian for a system coupled to a bath of harmoni
See, e.g., Settineri, Alessio, et al. "Dissipation and thermal noise in hybrid quantum systems in the ultrastrong-coupling regime." Physical Review A 98.5 (2018): 053834.
"""
function liouvillian_generalized(
- H::QuantumObject{MT,OperatorQuantumObject},
+ H::QuantumObject{Operator},
fields::Vector,
T_list::Vector{<:Real};
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 isa Nothing) ? H.dims : SVector(N_trunc)
+ dims = (N_trunc isa Nothing) ? H.dimensions : SVector(N_trunc)
final_size = prod(dims)
result = eigen(H)
E = real.(result.values[1:final_size])
- U = QuantumObject(result.vectors, result.type, result.dims)
+ U = QuantumObject(result.vectors, result.type, result.dimensions)
- H_d = QuantumObject(Diagonal(complex(E)), type = Operator, dims = dims)
+ H_d = QuantumObject(Diagonal(complex(E)), type = Operator(), dims = dims)
Ω = E' .- E
- Ωp = triu(dense_to_sparse(Ω, tol), 1)
+ Ωp = triu(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, σ), type = Operator, dims = dims)
- F1 = dense_to_sparse(F1, tol)
+ F1 = QuantumObject(gaussian.(Ω, 0, σ), type = Operator(), dims = dims)
+ F1 = to_sparse(F1, tol)
# Filter in the Liouville space
# M1 = ones(final_size, final_size)
@@ -255,23 +575,23 @@ function liouvillian_generalized(
Ω1 = kron(Ω, M1)
Ω2 = kron(M1, Ω)
Ωdiff = Ω1 .- Ω2
- F2 = QuantumObject(gaussian.(Ωdiff, 0, σ), SuperOperator, dims)
- F2 = dense_to_sparse(F2, tol)
+ F2 = QuantumObject(gaussian.(Ωdiff, 0, σ), SuperOperator(), dims)
+ F2 = to_sparse(F2, tol)
L = liouvillian(H_d)
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:final_size, 1:final_size], tol)
+ X_op = 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), 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)
+ N_th = n_thermal.(Ωp, T_list[i])
+ 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 +=
@@ -286,27 +606,27 @@ function liouvillian_generalized(
end
function _liouvillian_floquet(
- L₀::QuantumObject{<:AbstractArray{T1},SuperOperatorQuantumObject},
- Lₚ::QuantumObject{<:AbstractArray{T2},SuperOperatorQuantumObject},
- Lₘ::QuantumObject{<:AbstractArray{T3},SuperOperatorQuantumObject},
+ L₀::QuantumObject{SuperOperator},
+ Lₚ::QuantumObject{SuperOperator},
+ Lₘ::QuantumObject{SuperOperator},
ω::Real,
n_max::Int,
tol::Real,
-) where {T1,T2,T3}
+)
L_0 = L₀.data
L_p = Lₚ.data
L_m = Lₘ.data
- L_p_dense = sparse_to_dense(Lₚ.data)
- L_m_dense = sparse_to_dense(Lₘ.data)
+ L_p_dense = to_dense(Lₚ.data)
+ L_m_dense = to_dense(Lₘ.data)
S = -(L_0 - 1im * n_max * ω * I) \ L_p_dense
T = -(L_0 + 1im * n_max * ω * I) \ L_m_dense
- for n_i in n_max-1:-1:1
+ for n_i in (n_max-1):-1:1
S = -(L_0 - 1im * n_i * ω * I + L_m * S) \ L_p_dense
T = -(L_0 + 1im * n_i * ω * I + L_p * T) \ L_m_dense
end
- tol == 0 && return QuantumObject(L_0 + L_m * S + L_p * T, SuperOperator, L₀.dims)
- return QuantumObject(dense_to_sparse(L_0 + L_m * S + L_p * T, tol), SuperOperator, L₀.dims)
+ tol == 0 && return QuantumObject(L_0 + L_m * S + L_p * T, SuperOperator(), L₀.dimensions)
+ return QuantumObject(to_sparse(L_0 + L_m * S + L_p * T, tol), SuperOperator(), L₀.dimensions)
end
diff --git a/src/time_evolution/time_evolution_dynamical.jl b/src/time_evolution/time_evolution_dynamical.jl
index 9ee68fac3..305822611 100644
--- a/src/time_evolution/time_evolution_dynamical.jl
+++ b/src/time_evolution/time_evolution_dynamical.jl
@@ -8,12 +8,12 @@ function _reduce_dims(
sel,
reduce,
) where {T,N,DT<:Integer}
- nd = length(dims)
+ n_d = length(dims)
dims_new = zero(dims)
dims_new[sel] .= reduce
@. dims_new = dims - dims_new
- if nd == 1
+ if n_d == 1
ρmat = similar(QO, dims_new[1], dims_new[1])
copyto!(ρmat, view(QO, 1:dims_new[1], 1:dims_new[1]))
else
@@ -32,22 +32,22 @@ function _increase_dims(
sel,
increase,
) where {T,N,DT<:Integer}
- nd = length(dims)
+ n_d = length(dims)
dims_new = MVector(zero(dims)) # Mutable SVector
dims_new[sel] .= increase
@. dims_new = dims + dims_new
- if nd == 1
+ if n_d == 1
ρ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)
+ 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(vcat(dims_new, dims_new))...)
ρmat = reshape(QO, reverse(vcat(dims, dims))...)
for i in eachindex(sel)
- 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)
+ fill!(selectdim(ρmat2, n_d - sel[i] + 1, (dims[sel[i]]+1):dims_new[sel[i]]), 0)
+ fill!(selectdim(ρmat2, 2 * n_d - sel[i] + 1, (dims[sel[i]]+1):dims_new[sel[i]]), 0)
end
copyto!(view(ρmat2, reverse!(repeat([1:n for n in dims], 2))...), ρmat)
ρmat = reshape(ρmat2, prod(dims_new), prod(dims_new))
@@ -59,14 +59,14 @@ end
_dfd_set_pillow(dim)::Int = min(max(round(Int, 0.02 * dim), 1), 20)
function _DFDIncreaseReduceCondition(u, t, integrator)
- internal_params = integrator.p
- dim_list = internal_params.dim_list
- maxdims = internal_params.maxdims
- tol_list = internal_params.tol_list
- increase_list = internal_params.increase_list
- reduce_list = internal_params.reduce_list
- pillow_list = internal_params.pillow_list
- dfd_ρt_cache = internal_params.dfd_ρt_cache
+ params = integrator.p
+ dim_list = params.dim_list
+ maxdims = params.maxdims
+ tol_list = params.tol_list
+ increase_list = params.increase_list
+ reduce_list = params.reduce_list
+ pillow_list = params.pillow_list
+ dfd_ρt_cache = params.dfd_ρt_cache
# I need this cache because I can't reshape directly the integrator.u
copyto!(dfd_ρt_cache, u)
@@ -77,7 +77,7 @@ function _DFDIncreaseReduceCondition(u, t, integrator)
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, SVector(i))[1]
- @views res = norm(ρi[diagind(ρi)[end-pillow_i:end]], 1) * sqrt(dim_i) / pillow_i
+ @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
elseif res < tol_list[i] * 1e-2 && dim_i > 3
@@ -89,18 +89,18 @@ function _DFDIncreaseReduceCondition(u, t, integrator)
end
function _DFDIncreaseReduceAffect!(integrator)
- internal_params = integrator.p
- H = internal_params.H_fun
- c_ops = internal_params.c_ops_fun
- e_ops = internal_params.e_ops_fun
- dim_list = internal_params.dim_list
- increase_list = internal_params.increase_list
- reduce_list = internal_params.reduce_list
- pillow_list = internal_params.pillow_list
- dim_list_evo_times = internal_params.dim_list_evo_times
- dim_list_evo = internal_params.dim_list_evo
- dfd_ρt_cache = internal_params.dfd_ρt_cache
- dfd_params = internal_params.dfd_params
+ params = integrator.p
+ H = params.H_fun
+ c_ops = params.c_ops_fun
+ e_ops = params.e_ops_fun
+ dim_list = params.dim_list
+ increase_list = params.increase_list
+ reduce_list = params.reduce_list
+ pillow_list = params.pillow_list
+ dim_list_evo_times = params.dim_list_evo_times
+ dim_list_evo = params.dim_list_evo
+ dfd_ρt_cache = params.dfd_ρt_cache
+ dfd_params = params.dfd_params
ρt = vec2mat(dfd_ρt_cache)
@@ -122,34 +122,35 @@ function _DFDIncreaseReduceAffect!(integrator)
fill!(increase_list, false)
fill!(reduce_list, false)
push!(dim_list_evo_times, integrator.t)
- push!(dim_list_evo, dim_list)
+ push!(dim_list_evo, copy(dim_list))
e_ops2 = map(op -> mat2vec(get_data(op)'), e_ops(dim_list, dfd_params))
L = liouvillian(H(dim_list, dfd_params), c_ops(dim_list, dfd_params)).data
resize!(integrator, size(L, 1))
copyto!(integrator.u, mat2vec(ρt))
- integrator.p = merge(internal_params, (L = L, e_ops = e_ops2, dfd_ρt_cache = similar(integrator.u)))
+ # By doing this, we are assuming that the system is time-independent and f is a MatrixOperator
+ integrator.f = ODEFunction{true,FullSpecialize}(MatrixOperator(L))
+ integrator.p = merge(params, (dfd_ρt_cache = similar(integrator.u),))
+ _mesolve_callbacks_new_e_ops!(integrator, e_ops2)
return nothing
end
function dfd_mesolveProblem(
H::Function,
- ψ0::QuantumObject{<:AbstractArray{T1},StateOpType},
- t_l::AbstractVector,
+ ψ0::QuantumObject{StateOpType},
+ tlist::AbstractVector,
c_ops::Function,
maxdims::Vector{T2},
dfd_params::NamedTuple = NamedTuple();
- alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Function = (dim_list) -> Vector{Vector{T1}}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
+ e_ops::Function = (dim_list) -> Vector{Vector{eltype(ψ0)}}([]),
params::NamedTuple = NamedTuple(),
tol_list::Vector{<:Number} = fill(1e-8, length(maxdims)),
kwargs...,
-) where {T1,T2<:Integer,StateOpType<:Union{KetQuantumObject,OperatorQuantumObject}}
- length(ψ0.dims) != length(maxdims) &&
- throw(DimensionMismatch("'dim_list' and 'maxdims' do not have the same dimension."))
+) where {T2<:Integer,StateOpType<:Union{Ket,Operator}}
+ length(ψ0.dimensions) != length(maxdims) &&
+ throw(DimensionMismatch("`dim_list` and `maxdims` do not have the same dimension."))
dim_list = MVector(ψ0.dims)
H₀ = H(dim_list, dfd_params)
@@ -157,9 +158,9 @@ function dfd_mesolveProblem(
e_ops₀ = e_ops(dim_list, dfd_params)
dim_list_evo_times = [0.0]
- dim_list_evo = [dim_list]
- reduce_list = MVector(ntuple(i -> false, length(dim_list)))
- increase_list = MVector(ntuple(i -> false, length(dim_list)))
+ dim_list_evo = [copy(dim_list)]
+ reduce_list = MVector(ntuple(i -> false, Val(length(dim_list))))
+ increase_list = MVector(ntuple(i -> false, Val(length(dim_list))))
pillow_list = _dfd_set_pillow.(dim_list)
params2 = merge(
@@ -187,16 +188,15 @@ function dfd_mesolveProblem(
haskey(kwargs2, :callback) ? merge(kwargs2, (callback = CallbackSet(cb_dfd, kwargs2.callback),)) :
merge(kwargs2, (callback = cb_dfd,))
- return mesolveProblem(H₀, ψ0, t_l, c_ops₀; e_ops = e_ops₀, alg = alg, H_t = H_t, params = params2, kwargs2...)
+ return mesolveProblem(H₀, ψ0, tlist, c_ops₀; e_ops = e_ops₀, params = params2, kwargs2...)
end
@doc raw"""
dfd_mesolve(H::Function, ψ0::QuantumObject,
- t_l::AbstractVector, c_ops::Function, maxdims::AbstractVector,
+ tlist::AbstractVector, c_ops::Function, maxdims::AbstractVector,
dfd_params::NamedTuple=NamedTuple();
alg::OrdinaryDiffEqAlgorithm=Tsit5(),
e_ops::Function=(dim_list) -> Vector{Vector{T1}}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing,
params::NamedTuple=NamedTuple(),
tol_list::Vector{<:Number}=fill(1e-8, length(maxdims)),
kwargs...)
@@ -209,48 +209,44 @@ Time evolution of an open quantum system using master equation, dynamically chan
"""
function dfd_mesolve(
H::Function,
- ψ0::QuantumObject{<:AbstractArray{T1},StateOpType},
- t_l::AbstractVector,
+ ψ0::QuantumObject{StateOpType},
+ tlist::AbstractVector,
c_ops::Function,
maxdims::Vector{T2},
dfd_params::NamedTuple = NamedTuple();
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Function = (dim_list) -> Vector{Vector{T1}}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
+ e_ops::Function = (dim_list) -> Vector{Vector{eltype(ψ0)}}([]),
params::NamedTuple = NamedTuple(),
tol_list::Vector{<:Number} = fill(1e-8, length(maxdims)),
kwargs...,
-) where {T1,T2<:Integer,StateOpType<:Union{KetQuantumObject,OperatorQuantumObject}}
+) where {T2<:Integer,StateOpType<:Union{Ket,Operator}}
dfd_prob = dfd_mesolveProblem(
H,
ψ0,
- t_l,
+ tlist,
c_ops,
maxdims,
dfd_params;
- alg = alg,
e_ops = e_ops,
- H_t = H_t,
params = params,
tol_list = tol_list,
kwargs...,
)
- sol = solve(dfd_prob, alg)
+ sol = solve(dfd_prob.prob, alg)
- ρ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),
- )
+ ρt = map(eachindex(sol.t)) do i
+ idx = findfirst(>=(sol.t[i]), sol.prob.p.dim_list_evo_times)
+ idx2 = isnothing(idx) ? length(sol.prob.p.dim_list_evo) : (idx == 1 ? 1 : idx - 1)
+
+ return QuantumObject(vec2mat(sol.u[i]), type = Operator(), dims = sol.prob.p.dim_list_evo[idx2])
+ end
return TimeEvolutionSol(
+ dfd_prob.times,
sol.t,
ρt,
- sol.prob.p.expvals,
+ _get_expvals(sol, SaveFuncMESolve),
sol.retcode,
sol.alg,
sol.prob.kwargs[:abstol],
@@ -279,25 +275,23 @@ end
function _DSF_mesolve_Affect!(integrator)
internal_params = integrator.p
- op_l = internal_params.op_l
+ op_list = internal_params.op_list
op_l_vec = internal_params.op_l_vec
αt_list = internal_params.αt_list
δα_list = internal_params.δα_list
H = internal_params.H_fun
c_ops = internal_params.c_ops_fun
e_ops = internal_params.e_ops_fun
- e_ops_vec = internal_params.e_ops
dsf_cache = internal_params.dsf_cache
dsf_params = internal_params.dsf_params
expv_cache = internal_params.expv_cache
dsf_identity = internal_params.dsf_identity
dsf_displace_cache_full = internal_params.dsf_displace_cache_full
- op_l_length = length(op_l)
- fill!(dsf_displace_cache_full.coefficients, 0)
+ op_l_length = length(op_list)
- for i in eachindex(op_l)
- # op = op_l[i]
+ for i in eachindex(op_list)
+ # op = op_list[i]
op_vec = op_l_vec[i]
αt = αt_list[i]
δα = δα_list[i]
@@ -318,12 +312,17 @@ function _DSF_mesolve_Affect!(integrator)
# arnoldi!(expv_cache, Aᵢ, dsf_cache)
# expv!(integrator.u, expv_cache, one(αt), dsf_cache)
- dsf_displace_cache_full.coefficients[i] = Δα
- dsf_displace_cache_full.coefficients[i+op_l_length] = -conj(Δα)
- dsf_displace_cache_full.coefficients[i+2*op_l_length] = conj(Δα)
- dsf_displace_cache_full.coefficients[i+3*op_l_length] = -Δα
+ dsf_displace_cache_full.ops[i].λ.val = Δα
+ dsf_displace_cache_full.ops[i+op_l_length].λ.val = -conj(Δα)
+ dsf_displace_cache_full.ops[i+2*op_l_length].λ.val = conj(Δα)
+ dsf_displace_cache_full.ops[i+3*op_l_length].λ.val = -Δα
αt_list[i] += Δα
+ else
+ dsf_displace_cache_full.ops[i].λ.val = 0
+ dsf_displace_cache_full.ops[i+op_l_length].λ.val = 0
+ dsf_displace_cache_full.ops[i+2*op_l_length].λ.val = 0
+ dsf_displace_cache_full.ops[i+3*op_l_length].λ.val = 0
end
end
@@ -331,61 +330,56 @@ function _DSF_mesolve_Affect!(integrator)
arnoldi!(expv_cache, dsf_displace_cache_full, dsf_cache)
expv!(integrator.u, expv_cache, 1, dsf_cache)
- op_l2 = op_l .+ αt_list
+ op_l2 = op_list .+ αt_list
e_ops2 = e_ops(op_l2, dsf_params)
- _mat2vec_data = op -> mat2vec(get_data(op)')
- @. e_ops_vec = _mat2vec_data(e_ops2)
- return copyto!(internal_params.L, liouvillian(H(op_l2, dsf_params), c_ops(op_l2, dsf_params), dsf_identity).data)
+ _mesolve_callbacks_new_e_ops!(integrator, [_generate_mesolve_e_op(op) for op in e_ops2])
+ # By doing this, we are assuming that the system is time-independent and f is a MatrixOperator
+ copyto!(integrator.f.f.A, liouvillian(H(op_l2, dsf_params), c_ops(op_l2, dsf_params), dsf_identity).data)
+ return u_modified!(integrator, true)
end
function dsf_mesolveProblem(
H::Function,
- ψ0::QuantumObject{<:AbstractArray{T},StateOpType},
- t_l::AbstractVector,
+ ψ0::QuantumObject{StateOpType},
+ tlist::AbstractVector,
c_ops::Function,
- op_list::Vector{TOl},
+ op_list::Union{AbstractVector,Tuple},
α0_l::Vector{<:Number} = zeros(length(op_list)),
dsf_params::NamedTuple = NamedTuple();
- alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Function = (op_list, p) -> Vector{TOl}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
+ e_ops::Function = (op_list, p) -> (),
params::NamedTuple = NamedTuple(),
δα_list::Vector{<:Real} = fill(0.2, length(op_list)),
krylov_dim::Int = max(6, min(10, cld(length(ket2dm(ψ0).data), 4))),
kwargs...,
-) where {T,StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},TOl}
- op_l = op_list
- H₀ = H(op_l .+ α0_l, dsf_params)
- c_ops₀ = c_ops(op_l .+ α0_l, dsf_params)
- e_ops₀ = e_ops(op_l .+ α0_l, dsf_params)
+) where {StateOpType<:Union{Ket,Operator}}
+ op_list = deepcopy(op_list)
+ H₀ = H(op_list .+ α0_l, dsf_params)
+ c_ops₀ = c_ops(op_list .+ α0_l, dsf_params)
+ e_ops₀ = e_ops(op_list .+ α0_l, dsf_params)
+
+ T = eltype(ψ0)
αt_list = convert(Vector{T}, α0_l)
- op_l_vec = map(op -> mat2vec(get_data(op)'), op_l)
+ op_l_vec = map(op -> mat2vec(get_data(op)'), op_list)
# Create the Krylov subspace with kron(H₀.data, H₀.data) just for initialize
expv_cache = arnoldi(kron(H₀.data, H₀.data), mat2vec(ket2dm(ψ0).data), krylov_dim)
- dsf_identity = I(prod(H₀.dims))
- dsf_displace_cache_left = map(op -> Qobj(kron(op.data, dsf_identity)), op_l)
- dsf_displace_cache_left_dag = map(op -> Qobj(kron(sparse(op.data'), dsf_identity)), op_l)
- dsf_displace_cache_right = map(op -> Qobj(kron(dsf_identity, op.data)), op_l)
- dsf_displace_cache_right_dag = map(op -> Qobj(kron(dsf_identity, sparse(op.data'))), op_l)
- dsf_displace_cache_full = OperatorSum(
- zeros(length(op_l) * 4),
- vcat(
- dsf_displace_cache_left,
- dsf_displace_cache_left_dag,
- dsf_displace_cache_right,
- dsf_displace_cache_right_dag,
- ),
- )
+ dsf_identity = I(prod(H₀.dimensions))
+ dsf_displace_cache_left = sum(op -> ScalarOperator(one(T)) * MatrixOperator(kron(op.data, dsf_identity)), op_list)
+ dsf_displace_cache_left_dag =
+ sum(op -> ScalarOperator(one(T)) * MatrixOperator(kron(sparse(op.data'), dsf_identity)), op_list)
+ dsf_displace_cache_right = sum(op -> ScalarOperator(one(T)) * MatrixOperator(kron(dsf_identity, op.data)), op_list)
+ dsf_displace_cache_right_dag =
+ sum(op -> ScalarOperator(one(T)) * MatrixOperator(kron(dsf_identity, sparse(op.data'))), op_list)
+ dsf_displace_cache_full =
+ dsf_displace_cache_left + dsf_displace_cache_left_dag + dsf_displace_cache_right + dsf_displace_cache_right_dag
- params2 = params
params2 = merge(
params,
(
H_fun = H,
c_ops_fun = c_ops,
e_ops_fun = e_ops,
- op_l = op_l,
+ op_list = op_list,
op_l_vec = op_l_vec,
αt_list = αt_list,
δα_list = δα_list,
@@ -403,19 +397,18 @@ function dsf_mesolveProblem(
haskey(kwargs2, :callback) ? merge(kwargs2, (callback = CallbackSet(cb_dsf, kwargs2.callback),)) :
merge(kwargs2, (callback = cb_dsf,))
- return mesolveProblem(H₀, ψ0, t_l, c_ops₀; e_ops = e_ops₀, alg = alg, H_t = H_t, params = params2, kwargs2...)
+ return mesolveProblem(H₀, ψ0, tlist, c_ops₀; e_ops = e_ops₀, params = params2, kwargs2...)
end
@doc raw"""
dsf_mesolve(H::Function,
ψ0::QuantumObject,
- t_l::AbstractVector, c_ops::Function,
+ tlist::AbstractVector, c_ops::Function,
op_list::Vector{TOl},
α0_l::Vector{<:Number}=zeros(length(op_list)),
dsf_params::NamedTuple=NamedTuple();
alg::OrdinaryDiffEqAlgorithm=Tsit5(),
e_ops::Function=(op_list,p) -> Vector{TOl}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing,
params::NamedTuple=NamedTuple(),
δα_list::Vector{<:Number}=fill(0.2, length(op_list)),
krylov_dim::Int=max(6, min(10, cld(length(ket2dm(ψ0).data), 4))),
@@ -429,31 +422,28 @@ Time evolution of an open quantum system using master equation and the Dynamical
"""
function dsf_mesolve(
H::Function,
- ψ0::QuantumObject{<:AbstractArray{T},StateOpType},
- t_l::AbstractVector,
+ ψ0::QuantumObject{StateOpType},
+ tlist::AbstractVector,
c_ops::Function,
- op_list::Vector{TOl},
+ op_list::Union{AbstractVector,Tuple},
α0_l::Vector{<:Number} = zeros(length(op_list)),
dsf_params::NamedTuple = NamedTuple();
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Function = (op_list, p) -> Vector{TOl}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
+ e_ops::Function = (op_list, p) -> (),
params::NamedTuple = NamedTuple(),
δα_list::Vector{<:Real} = fill(0.2, length(op_list)),
krylov_dim::Int = max(6, min(10, cld(length(ket2dm(ψ0).data), 4))),
kwargs...,
-) where {T,StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},TOl}
+) where {StateOpType<:Union{Ket,Operator}}
dsf_prob = dsf_mesolveProblem(
H,
ψ0,
- t_l,
+ tlist,
c_ops,
op_list,
α0_l,
dsf_params;
- alg = alg,
e_ops = e_ops,
- H_t = H_t,
params = params,
δα_list = δα_list,
krylov_dim = krylov_dim,
@@ -465,31 +455,29 @@ end
function dsf_mesolve(
H::Function,
- ψ0::QuantumObject{<:AbstractArray{T},StateOpType},
- t_l::AbstractVector,
- op_list::Vector{TOl},
+ ψ0::QuantumObject{StateOpType},
+ tlist::AbstractVector,
+ op_list::Union{AbstractVector,Tuple},
α0_l::Vector{<:Number} = zeros(length(op_list)),
dsf_params::NamedTuple = NamedTuple();
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Function = (op_list, p) -> Vector{TOl}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
+ e_ops::Function = (op_list, p) -> (),
params::NamedTuple = NamedTuple(),
δα_list::Vector{<:Real} = fill(0.2, length(op_list)),
krylov_dim::Int = max(6, min(10, cld(length(ket2dm(ψ0).data), 4))),
kwargs...,
-) where {T,StateOpType<:Union{KetQuantumObject,OperatorQuantumObject},TOl}
- c_ops = op_list -> Vector{TOl}([])
+) where {StateOpType<:Union{Ket,Operator}}
+ c_ops = op_list -> ()
return dsf_mesolve(
H,
ψ0,
- t_l,
+ tlist,
c_ops,
op_list,
α0_l,
dsf_params;
alg = alg,
e_ops = e_ops,
- H_t = H_t,
params = params,
δα_list = δα_list,
krylov_dim = krylov_dim,
@@ -500,15 +488,15 @@ end
# Dynamical Shifted Fock mcsolve
function _DSF_mcsolve_Condition(u, t, integrator)
- internal_params = integrator.p
- op_l = internal_params.op_l
- δα_list = internal_params.δα_list
+ params = integrator.p
+ op_list = params.op_list
+ δα_list = params.δα_list
ψt = u
condition = false
- @inbounds for i in eachindex(op_l)
- op = op_l[i]
+ @inbounds for i in eachindex(op_list)
+ op = op_list[i]
δα = δα_list[i]
Δα = dot(ψt, op.data, ψt) / dot(ψt, ψt)
if δα < abs(Δα)
@@ -519,29 +507,32 @@ function _DSF_mcsolve_Condition(u, t, integrator)
end
function _DSF_mcsolve_Affect!(integrator)
- internal_params = integrator.p
- op_l = internal_params.op_l
- αt_list = internal_params.αt_list
- δα_list = internal_params.δα_list
- H = internal_params.H_fun
- c_ops = internal_params.c_ops_fun
- e_ops = internal_params.e_ops_fun
- e_ops0 = internal_params.e_ops_mc
- c_ops0 = internal_params.c_ops
- ψt = internal_params.dsf_cache1
- dsf_cache = internal_params.dsf_cache2
- expv_cache = internal_params.expv_cache
- dsf_params = internal_params.dsf_params
- dsf_displace_cache_full = internal_params.dsf_displace_cache_full
+ params = integrator.p
+ op_list = params.op_list
+ αt_list = params.αt_list
+ δα_list = params.δα_list
+ H = params.H_fun
+ c_ops = params.c_ops_fun
+ e_ops = params.e_ops_fun
+ ψt = params.dsf_cache1
+ dsf_cache = params.dsf_cache2
+ expv_cache = params.expv_cache
+ dsf_params = params.dsf_params
+ dsf_displace_cache_full = params.dsf_displace_cache_full
+
+ # e_ops0 = params.e_ops
+ # c_ops0 = params.c_ops
+
+ e_ops0 = _get_e_ops(integrator, SaveFuncMCSolve)
+ c_ops0, c_ops0_herm = _mcsolve_get_c_ops(integrator)
copyto!(ψt, integrator.u)
normalize!(ψt)
- op_l_length = length(op_l)
- fill!(dsf_displace_cache_full.coefficients, 0)
+ op_l_length = length(op_list)
- for i in eachindex(op_l)
- op = op_l[i]
+ for i in eachindex(op_list)
+ op = op_list[i]
αt = αt_list[i]
δα = δα_list[i]
Δα = dot(ψt, op.data, ψt)
@@ -556,10 +547,13 @@ function _DSF_mcsolve_Affect!(integrator)
# arnoldi!(expv_cache, Aᵢ, dsf_cache)
# expv!(integrator.u, expv_cache, one(αt), dsf_cache)
- dsf_displace_cache_full.coefficients[i] = conj(Δα)
- dsf_displace_cache_full.coefficients[i+op_l_length] = -Δα
+ dsf_displace_cache_full.ops[i].λ.val = conj(Δα)
+ dsf_displace_cache_full.ops[i+op_l_length].λ.val = -Δα
αt_list[i] += Δα
+ else
+ dsf_displace_cache_full.ops[i].λ.val = 0
+ dsf_displace_cache_full.ops[i+op_l_length].λ.val = 0
end
end
@@ -567,75 +561,74 @@ function _DSF_mcsolve_Affect!(integrator)
arnoldi!(expv_cache, dsf_displace_cache_full, dsf_cache)
expv!(integrator.u, expv_cache, 1, dsf_cache)
- op_l2 = op_l .+ αt_list
+ op_l2 = op_list .+ αt_list
e_ops2 = e_ops(op_l2, dsf_params)
c_ops2 = c_ops(op_l2, dsf_params)
+
+ ## By copying the data, we are assuming that the variables are Vectors and not Tuple
@. e_ops0 = get_data(e_ops2)
@. c_ops0 = get_data(c_ops2)
- H_eff = H(op_l2, dsf_params).data - lmul!(convert(eltype(ψt), 0.5im), mapreduce(op -> op' * op, +, c_ops0))
- return mul!(internal_params.U, -1im, H_eff)
+ c_ops0_herm .= map(op -> op' * op, c_ops0)
+
+ H_nh = convert(eltype(ψt), 0.5im) * sum(c_ops0_herm)
+ # By doing this, we are assuming that the system is time-independent and f is a MatrixOperator
+ copyto!(integrator.f.f.A, lmul!(-1im, H(op_l2, dsf_params).data - H_nh))
+ return u_modified!(integrator, true)
end
function _dsf_mcsolve_prob_func(prob, i, repeat)
- internal_params = prob.p
+ params = prob.p
prm = merge(
- internal_params,
+ params,
(
- U = copy(internal_params.U),
- e_ops_mc = copy(internal_params.e_ops_mc),
- c_ops = copy(internal_params.c_ops),
- expvals = similar(internal_params.expvals),
- cache_mc = similar(internal_params.cache_mc),
- weights_mc = similar(internal_params.weights_mc),
- cumsum_weights_mc = similar(internal_params.weights_mc),
- random_n = Ref(rand()),
- progr_mc = ProgressBar(size(internal_params.expvals, 2), enable = false),
- jump_times_which_idx = Ref(1),
- jump_times = similar(internal_params.jump_times),
- jump_which = similar(internal_params.jump_which),
- αt_list = copy(internal_params.αt_list),
- dsf_cache1 = similar(internal_params.dsf_cache1),
- dsf_cache2 = similar(internal_params.dsf_cache2),
- expv_cache = copy(internal_params.expv_cache),
- dsf_displace_cache_full = OperatorSum(
- copy(internal_params.dsf_displace_cache_full.coefficients),
- internal_params.dsf_displace_cache_full.operators,
- ),
+ αt_list = copy(params.αt_list),
+ dsf_cache1 = similar(params.dsf_cache1),
+ dsf_cache2 = similar(params.dsf_cache2),
+ expv_cache = copy(params.expv_cache),
+ dsf_displace_cache_full = deepcopy(params.dsf_displace_cache_full), # This brutally copies also the MatrixOperators, and it is inefficient.
),
)
- return remake(prob, p = prm)
+ f = deepcopy(prob.f.f)
+
+ # We need to deepcopy the callbacks because they contain the c_ops and e_ops, which are modified in the affect function. They also contain all the cache variables needed for mcsolve.
+ cb = deepcopy(prob.kwargs[:callback])
+
+ return remake(prob, f = f, p = prm, callback = cb)
end
function dsf_mcsolveEnsembleProblem(
H::Function,
- ψ0::QuantumObject{<:AbstractArray{T},KetQuantumObject},
- t_l::AbstractVector,
+ ψ0::QuantumObject{Ket},
+ tlist::AbstractVector,
c_ops::Function,
- op_list::Vector{TOl},
+ op_list::Union{AbstractVector,Tuple},
α0_l::Vector{<:Number} = zeros(length(op_list)),
dsf_params::NamedTuple = NamedTuple();
- alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Function = (op_list, p) -> Vector{TOl}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
+ e_ops::Function = (op_list, p) -> (),
params::NamedTuple = NamedTuple(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
δα_list::Vector{<:Real} = fill(0.2, length(op_list)),
jump_callback::TJC = ContinuousLindbladJumpCallback(),
krylov_dim::Int = min(5, cld(length(ψ0.data), 3)),
+ progress_bar::Union{Bool,Val} = Val(true),
kwargs...,
-) where {T,TOl,TJC<:LindbladJumpCallbackType}
- op_l = op_list
- H₀ = H(op_l .+ α0_l, dsf_params)
- c_ops₀ = c_ops(op_l .+ α0_l, dsf_params)
- e_ops₀ = e_ops(op_l .+ α0_l, dsf_params)
+) where {TJC<:LindbladJumpCallbackType}
+ op_list = deepcopy(op_list)
+ H₀ = H(op_list .+ α0_l, dsf_params)
+ c_ops₀ = c_ops(op_list .+ α0_l, dsf_params)
+ e_ops₀ = e_ops(op_list .+ α0_l, dsf_params)
+
+ T = eltype(ψ0)
αt_list = convert(Vector{T}, α0_l)
expv_cache = arnoldi(H₀.data, ψ0.data, krylov_dim)
- dsf_displace_cache = map(op -> Qobj(op.data), op_l)
- dsf_displace_cache_dag = map(op -> Qobj(sparse(op.data')), op_l)
- dsf_displace_cache_full = OperatorSum(zeros(length(op_l) * 2), vcat(dsf_displace_cache, dsf_displace_cache_dag))
+ dsf_displace_cache = sum(op -> ScalarOperator(one(T)) * MatrixOperator(op.data), op_list)
+ dsf_displace_cache_dag = sum(op -> ScalarOperator(one(T)) * MatrixOperator(sparse(op.data')), op_list)
+ dsf_displace_cache_full = dsf_displace_cache + dsf_displace_cache_dag
params2 = merge(
params,
@@ -643,7 +636,7 @@ function dsf_mcsolveEnsembleProblem(
H_fun = H,
c_ops_fun = c_ops,
e_ops_fun = e_ops,
- op_l = op_l,
+ op_list = op_list,
αt_list = αt_list,
δα_list = δα_list,
dsf_cache1 = similar(ψ0.data),
@@ -663,14 +656,15 @@ function dsf_mcsolveEnsembleProblem(
return mcsolveEnsembleProblem(
H₀,
ψ0,
- t_l,
+ tlist,
c_ops₀;
e_ops = e_ops₀,
- alg = alg,
- H_t = H_t,
params = params2,
+ ntraj = ntraj,
+ ensemblealg = ensemblealg,
jump_callback = jump_callback,
prob_func = _dsf_mcsolve_prob_func,
+ progress_bar = progress_bar,
kwargs2...,
)
end
@@ -678,19 +672,19 @@ end
@doc raw"""
dsf_mcsolve(H::Function,
ψ0::QuantumObject,
- t_l::AbstractVector, c_ops::Function,
+ tlist::AbstractVector, c_ops::Function,
op_list::Vector{TOl},
α0_l::Vector{<:Number}=zeros(length(op_list)),
dsf_params::NamedTuple=NamedTuple();
alg::OrdinaryDiffEqAlgorithm=Tsit5(),
e_ops::Function=(op_list,p) -> Vector{TOl}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing,
params::NamedTuple=NamedTuple(),
δα_list::Vector{<:Real}=fill(0.2, length(op_list)),
- n_traj::Int=1,
- ensemble_method=EnsembleThreads(),
+ ntraj::Int=500,
+ ensemblealg::EnsembleAlgorithm=EnsembleThreads(),
jump_callback::LindbladJumpCallbackType=ContinuousLindbladJumpCallback(),
krylov_dim::Int=max(6, min(10, cld(length(ket2dm(ψ0).data), 4))),
+ progress_bar::Union{Bool,Val} = Val(true)
kwargs...)
Time evolution of a quantum system using the Monte Carlo wave function method and the Dynamical Shifted Fock algorithm.
@@ -701,40 +695,42 @@ Time evolution of a quantum system using the Monte Carlo wave function method an
"""
function dsf_mcsolve(
H::Function,
- ψ0::QuantumObject{<:AbstractArray{T},KetQuantumObject},
- t_l::AbstractVector,
+ ψ0::QuantumObject{Ket},
+ tlist::AbstractVector,
c_ops::Function,
- op_list::Vector{TOl},
+ op_list::Union{AbstractVector,Tuple},
α0_l::Vector{<:Number} = zeros(length(op_list)),
dsf_params::NamedTuple = NamedTuple();
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
- e_ops::Function = (op_list, p) -> Vector{TOl}([]),
- H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing,
+ e_ops::Function = (op_list, p) -> (),
params::NamedTuple = NamedTuple(),
δα_list::Vector{<:Real} = fill(0.2, length(op_list)),
- n_traj::Int = 1,
- ensemble_method = EnsembleThreads(),
+ ntraj::Int = 500,
+ ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
jump_callback::TJC = ContinuousLindbladJumpCallback(),
krylov_dim::Int = min(5, cld(length(ψ0.data), 3)),
+ progress_bar::Union{Bool,Val} = Val(true),
kwargs...,
-) where {T,TOl,TJC<:LindbladJumpCallbackType}
+) where {TJC<:LindbladJumpCallbackType}
ens_prob_mc = dsf_mcsolveEnsembleProblem(
H,
ψ0,
- t_l,
+ tlist,
c_ops,
op_list,
α0_l,
dsf_params;
alg = alg,
e_ops = e_ops,
- H_t = H_t,
params = params,
+ ntraj = ntraj,
+ ensemblealg = ensemblealg,
δα_list = δα_list,
jump_callback = jump_callback,
krylov_dim = krylov_dim,
+ progress_bar = progress_bar,
kwargs...,
)
- return mcsolve(ens_prob_mc; alg = alg, n_traj = n_traj, ensemble_method = ensemble_method)
+ return mcsolve(ens_prob_mc, alg, ntraj, ensemblealg)
end
diff --git a/src/utilities.jl b/src/utilities.jl
index 63d5a6d41..e30d6785c 100644
--- a/src/utilities.jl
+++ b/src/utilities.jl
@@ -3,7 +3,8 @@ Utilities:
internal (or external) functions which will be used throughout the entire package
=#
-export gaussian, n_th
+export gaussian, n_thermal
+export PhysicalConstants, convert_unit
export row_major_reshape, meshgrid
@doc raw"""
@@ -34,19 +35,105 @@ where ``\mu`` and ``\sigma^2`` are the mean and the variance respectively.
gaussian(x::Number, μ::Number, σ::Number) = exp(-(x - μ)^2 / (2 * σ^2))
@doc raw"""
- n_th(ω::Number, T::Real)
+ n_thermal(ω::Real, ω_th::Real)
-Gives the mean number of excitations in a mode with frequency ω at temperature T:
-``n_{\rm th} (\omega, T) = \frac{1}{e^{\omega/T} - 1}``
+Return the number of photons in thermal equilibrium for an harmonic oscillator mode with frequency ``\omega``, at the temperature described by ``\omega_{\textrm{th}} \equiv k_B T / \hbar``:
+```math
+n(\omega, \omega_{\textrm{th}}) = \frac{1}{e^{\omega/\omega_{\textrm{th}}} - 1},
+```
+where ``\hbar`` is the reduced Planck constant, and ``k_B`` is the Boltzmann constant.
"""
-function n_th(ω::Real, T::Real)::Float64
- (T == 0 || ω == 0) && return 0.0
- abs(ω / T) > 50 && return 0.0
- return 1 / (exp(ω / T) - 1)
+function n_thermal(ω::T1, ω_th::T2) where {T1<:Real,T2<:Real}
+ x = exp(ω / ω_th)
+ n = ((x != 1) && (ω_th > 0)) ? 1 / (x - 1) : 0
+ return _float_type(promote_type(T1, T2))(n)
end
-_get_dense_similar(A::AbstractArray, args...) = similar(A, args...)
-_get_dense_similar(A::AbstractSparseMatrix, args...) = similar(nonzeros(A), args...)
+@doc raw"""
+ const PhysicalConstants
+
+A `NamedTuple` which stores some constant values listed in [*CODATA recommended values of the fundamental physical constants: 2022*](https://physics.nist.gov/cuu/pdf/wall_2022.pdf)
+
+The current stored constants are:
+- `c` : (exact) speed of light in vacuum with unit ``[\textrm{m}\cdot\textrm{s}^{-1}]``
+- `G` : Newtonian constant of gravitation with unit ``[\textrm{m}^3\cdot\textrm{kg}^{−1}\cdot\textrm{s}^{−2}]``
+- `h` : (exact) Planck constant with unit ``[\textrm{J}\cdot\textrm{s}]``
+- `ħ` : reduced Planck constant with unit ``[\textrm{J}\cdot\textrm{s}]``
+- `e` : (exact) elementary charge with unit ``[\textrm{C}]``
+- `μ0` : vacuum magnetic permeability with unit ``[\textrm{N}\cdot\textrm{A}^{-2}]``
+- `ϵ0` : vacuum electric permittivity with unit ``[\textrm{F}\cdot\textrm{m}^{-1}]``
+- `k` : (exact) Boltzmann constant with unit ``[\textrm{J}\cdot\textrm{K}^{-1}]``
+- `NA` : (exact) Avogadro constant with unit ``[\textrm{mol}^{-1}]``
+
+# Examples
+
+```jldoctest
+julia> PhysicalConstants.ħ
+1.0545718176461565e-34
+```
+"""
+const PhysicalConstants = (
+ c = 299792458.0,
+ G = 6.67430e-11,
+ h = 6.62607015e-34,
+ ħ = 6.62607015e-34 / (2 * π),
+ e = 1.602176634e-19,
+ μ0 = 1.25663706127e-6,
+ ϵ0 = 8.8541878188e-12,
+ k = 1.380649e-23,
+ NA = 6.02214076e23,
+)
+
+# common energy units (the values below are all in the unit of Joule)
+const _energy_units::Dict{Symbol,Float64} = Dict(
+ :J => 1.0,
+ :eV => PhysicalConstants.e,
+ :meV => 1.0e-3 * PhysicalConstants.e,
+ :MHz => 1.0e6 * PhysicalConstants.h,
+ :GHz => 1.0e9 * PhysicalConstants.h,
+ :K => PhysicalConstants.k,
+ :mK => 1.0e-3 * PhysicalConstants.k,
+)
+
+@doc raw"""
+ convert_unit(value::Real, unit1::Symbol, unit2::Symbol)
+
+Convert the energy `value` from `unit1` to `unit2`. The `unit1` and `unit2` can be either the following `Symbol`:
+- `:J` : Joule
+- `:eV` : electron volt
+- `:meV` : milli-electron volt
+- `:MHz` : Mega-Hertz multiplied by Planck constant ``h``
+- `:GHz` : Giga-Hertz multiplied by Planck constant ``h``
+- `:K` : Kelvin multiplied by Boltzmann constant ``k``
+- `:mK` : milli-Kelvin multiplied by Boltzmann constant ``k``
+
+Note that we use the values stored in [`PhysicalConstants`](@ref) to do the conversion.
+
+# Examples
+
+```jldoctest
+julia> convert_unit(1, :eV, :J)
+1.602176634e-19
+
+julia> convert_unit(1, :GHz, :J)
+6.62607015e-25
+
+julia> round(convert_unit(1, :meV, :mK), digits=4)
+11604.5181
+```
+"""
+function convert_unit(value::T, unit1::Symbol, unit2::Symbol) where {T<:Real}
+ !haskey(_energy_units, unit1) && throw(ArgumentError("Invalid unit :$(unit1)"))
+ !haskey(_energy_units, unit2) && throw(ArgumentError("Invalid unit :$(unit2)"))
+ return _float_type(T)(value * (_energy_units[unit1] / _energy_units[unit2]))
+end
+
+get_typename_wrapper(A) = Base.typename(typeof(A)).wrapper
+
+_dense_similar(A::AbstractArray, args...) = similar(A, args...)
+_dense_similar(A::AbstractSparseMatrix, args...) = similar(nonzeros(A), args...)
+
+_sparse_similar(A::AbstractArray, args...) = sparse(args...)
_Ginibre_ensemble(n::Int, rank::Int = n) = randn(ComplexF64, n, rank) / sqrt(n)
@@ -54,3 +141,63 @@ makeVal(x::Val{T}) where {T} = x
makeVal(x) = Val(x)
getVal(x::Val{T}) where {T} = T
+getVal(x) = x # getVal for any other type
+
+_get_size(A::AbstractMatrix) = size(A)
+_get_size(A::AbstractVector) = (length(A), 1)
+_get_size(A::AbstractSciMLOperator) = size(A)
+
+_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))` instead of `$argname = $arg`. " *
+ "Alternatively, you can do `import QuantumToolbox: SVector` " *
+ "and use `$argname = SVector(" *
+ join(arg, ", ") *
+ ")`." maxlog = 1
+
+# lazy tensor warning
+for AType in (:AbstractArray, :AbstractSciMLOperator)
+ for BType in (:AbstractArray, :AbstractSciMLOperator)
+ if AType == BType == :AbstractArray
+ @eval begin
+ _lazy_tensor_warning(::$AType, ::$BType) = nothing
+ end
+ else
+ @eval begin
+ _lazy_tensor_warning(A::$AType, B::$BType) =
+ @warn "using lazy tensor (which can hurt performance) between data types: $(get_typename_wrapper(A)) and $(get_typename_wrapper(B))"
+ end
+ end
+ end
+end
+
+# functions for getting Float or Complex element type
+_float_type(::AbstractArray{T}) where {T<:Number} = _float_type(T)
+_float_type(::Type{Int32}) = Float32
+_float_type(::Type{Int64}) = Float64
+_float_type(::Type{Float32}) = Float32
+_float_type(::Type{Float64}) = Float64
+_float_type(::Type{Complex{Int32}}) = Float32
+_float_type(::Type{Complex{Int64}}) = Float64
+_float_type(::Type{Complex{Float32}}) = Float32
+_float_type(::Type{Complex{Float64}}) = Float64
+_float_type(T::Type{<:Real}) = T # Allow other untracked Real types, like ForwardDiff.Dual
+_complex_float_type(::AbstractArray{T}) where {T<:Number} = _complex_float_type(T)
+_complex_float_type(::Type{Int32}) = ComplexF32
+_complex_float_type(::Type{Int64}) = ComplexF64
+_complex_float_type(::Type{Float32}) = ComplexF32
+_complex_float_type(::Type{Float64}) = ComplexF64
+_complex_float_type(::Type{Complex{Int32}}) = ComplexF32
+_complex_float_type(::Type{Complex{Int64}}) = ComplexF64
+_complex_float_type(::Type{Complex{Float32}}) = ComplexF32
+_complex_float_type(::Type{Complex{Float64}}) = ComplexF64
+_complex_float_type(T::Type{<:Complex}) = T # Allow other untracked Complex types, like ForwardDiff.Dual
+
+_convert_eltype_wordsize(::Type{T}, ::Val{64}) where {T<:Int} = Int64
+_convert_eltype_wordsize(::Type{T}, ::Val{32}) where {T<:Int} = Int32
+_convert_eltype_wordsize(::Type{T}, ::Val{64}) where {T<:AbstractFloat} = Float64
+_convert_eltype_wordsize(::Type{T}, ::Val{32}) where {T<:AbstractFloat} = Float32
+_convert_eltype_wordsize(::Type{Complex{T}}, ::Val{64}) where {T<:Union{Int,AbstractFloat}} = ComplexF64
+_convert_eltype_wordsize(::Type{Complex{T}}, ::Val{32}) where {T<:Union{Int,AbstractFloat}} = ComplexF32
diff --git a/src/versioninfo.jl b/src/versioninfo.jl
index 4b67e3116..d2830ce0c 100644
--- a/src/versioninfo.jl
+++ b/src/versioninfo.jl
@@ -2,7 +2,7 @@
Command line output of information on QuantumToolbox, dependencies, and system information
=#
-"""
+@doc raw"""
QuantumToolbox.versioninfo(io::IO=stdout)
Command line output of information on QuantumToolbox, dependencies, and system information, same as [`QuantumToolbox.about`](@ref).
@@ -20,35 +20,48 @@ function versioninfo(io::IO = stdout)
"≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡\n",
"Copyright © QuTiP team 2022 and later.\n",
"Current admin team:\n",
- " Alberto Mercurio, Luca Gravina, Yi-Te Huang\n",
+ " Alberto Mercurio and Yi-Te Huang\n",
)
- # print package informations
+ # print package information
println(
io,
"Package information:\n",
"====================================\n",
"Julia Ver. $(VERSION)\n",
"QuantumToolbox Ver. $(_get_pkg_version("QuantumToolbox"))\n",
+ "SciMLOperators Ver. $(_get_pkg_version("SciMLOperators"))\n",
"LinearSolve Ver. $(_get_pkg_version("LinearSolve"))\n",
"OrdinaryDiffEqCore Ver. $(_get_pkg_version("OrdinaryDiffEqCore"))\n",
)
- # print System informations
- 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""")
- println(io, """WORD_SIZE: $(Sys.WORD_SIZE)""")
- println(io, """LIBM : $(Base.libm_name)""")
- println(io, """LLVM : libLLVM-$(Base.libllvm_version) ($(Sys.JIT), $(Sys.CPU_NAME))""")
- println(io, """BLAS : $(basename(BLAS_info.libname)) ($(BLAS_info.interface))""")
- println(io, """Threads : $(Threads.nthreads()) (on $(Sys.CPU_THREADS) virtual cores)""")
- return print(io, "\n")
+ # print System information
+ println(
+ io,
+ "System information:\n",
+ "====================================\n",
+ """OS : $(OS_name) ($(Sys.MACHINE))\n""",
+ """CPU : $(length(cpu)) × $(cpu[1].model)\n""",
+ """Memory : $(round(Sys.total_memory() / 2 ^ 30, digits=3)) GB\n""",
+ """WORD_SIZE: $(Sys.WORD_SIZE)\n""",
+ """LIBM : $(Base.libm_name)\n""",
+ """LLVM : libLLVM-$(Base.libllvm_version) ($(Sys.JIT), $(Sys.CPU_NAME))\n""",
+ """BLAS : $(basename(BLAS_info.libname)) ($(BLAS_info.interface))\n""",
+ """Threads : $(Threads.nthreads()) (on $(Sys.CPU_THREADS) virtual cores)\n""",
+ )
+
+ # print citation information
+ println(
+ io,
+ "+---------------------------------------------------+\n",
+ "| Please cite QuantumToolbox.jl in your publication |\n",
+ "+---------------------------------------------------+\n",
+ "For your convenience, a bibtex reference can be easily generated using `QuantumToolbox.cite()`.\n",
+ )
+ return nothing
end
-"""
+@doc raw"""
QuantumToolbox.about(io::IO=stdout)
Command line output of information on QuantumToolbox, dependencies, and system information, same as [`QuantumToolbox.versioninfo`](@ref).
@@ -63,3 +76,27 @@ function _get_pkg_version(pkg_name::String)
end
end
end
+
+@doc raw"""
+ QuantumToolbox.cite(io::IO = stdout)
+
+Command line output of citation information and bibtex generator for `QuantumToolbox.jl`.
+"""
+function cite(io::IO = stdout)
+ citation = raw"""
+ @article{QuantumToolbox.jl2025,
+ title = {Quantum{T}oolbox.jl: {A}n efficient {J}ulia framework for simulating open quantum systems},
+ author = {Mercurio, Alberto and Huang, Yi-Te and Cai, Li-Xun and Chen, Yueh-Nan and Savona, Vincenzo and Nori, Franco},
+ journal = {{Quantum}},
+ issn = {2521-327X},
+ publisher = {{Verein zur F{\\"{o}}rderung des Open Access Publizierens in den Quantenwissenschaften}},
+ volume = {9},
+ pages = {1866},
+ month = sep,
+ year = {2025},
+ doi = {10.22331/q-2025-09-29-1866},
+ url = {https://doi.org/10.22331/q-2025-09-29-1866}
+ }
+ """
+ return print(io, citation)
+end
diff --git a/src/visualization.jl b/src/visualization.jl
new file mode 100644
index 000000000..9e240cd63
--- /dev/null
+++ b/src/visualization.jl
@@ -0,0 +1,563 @@
+export plot_wigner
+export plot_fock_distribution
+export plot_bloch, Bloch, render, add_points!, add_vectors!, add_line!, add_arc!, clear!, add_states!
+
+@doc raw"""
+ plot_wigner(
+ state::QuantumObject{OpType};
+ library::Union{Val,Symbol}=Val(:Makie),
+ kwargs...
+ ) where {OpType<:Union{Bra,Ket,Operator}
+
+Plot the [Wigner quasipropability distribution](https://en.wikipedia.org/wiki/Wigner_quasiprobability_distribution) of `state` using the [`wigner`](@ref) function.
+
+The `library` keyword argument specifies the plotting library to use, defaulting to [`Makie.jl`](https://github.com/MakieOrg/Makie.jl).
+
+# Arguments
+- `state::QuantumObject`: The quantum state for which to plot the Wigner distribution.
+- `library::Union{Val,Symbol}`: The plotting library to use. Default is `Val(:Makie)`.
+- `kwargs...`: Additional keyword arguments to pass to the plotting function. See the documentation for the specific plotting library for more information.
+
+!!! note "Import library first"
+ The plotting libraries must first be imported before using them with this function.
+
+!!! warning "Beware of type-stability!"
+ If you want to keep type stability, it is recommended to use `Val(:Makie)` instead of `:Makie` as the plotting library. 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.
+"""
+plot_wigner(
+ state::QuantumObject{OpType};
+ library::Union{Val,Symbol} = Val(:Makie),
+ kwargs...,
+) where {OpType<:Union{Bra,Ket,Operator}} = plot_wigner(makeVal(library), state; kwargs...)
+
+plot_wigner(::Val{T}, state::QuantumObject{OpType}; kwargs...) where {T,OpType<:Union{Bra,Ket,Operator}} =
+ throw(ArgumentError("The specified plotting library $T is not available. Try running `using $T` first."))
+
+@doc raw"""
+ plot_fock_distribution(
+ ρ::QuantumObject{SType};
+ library::Union{Val, Symbol} = Val(:Makie),
+ kwargs...
+ ) where {SType<:Union{Ket,Operator}}
+
+Plot the [Fock state](https://en.wikipedia.org/wiki/Fock_state) distribution of `ρ`.
+
+The `library` keyword argument specifies the plotting library to use, defaulting to [`Makie`](https://github.com/MakieOrg/Makie.jl).
+
+# Arguments
+- `ρ::QuantumObject`: The quantum state for which to plot the Fock state distribution.
+- `library::Union{Val,Symbol}`: The plotting library to use. Default is `Val(:Makie)`.
+- `kwargs...`: Additional keyword arguments to pass to the plotting function. See the documentation for the specific plotting library for more information.
+
+!!! note "Import library first"
+ The plotting libraries must first be imported before using them with this function.
+
+!!! warning "Beware of type-stability!"
+ If you want to keep type stability, it is recommended to use `Val(:Makie)` instead of `:Makie` as the plotting library. 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.
+"""
+plot_fock_distribution(
+ ρ::QuantumObject{SType};
+ library::Union{Val,Symbol} = Val(:Makie),
+ kwargs...,
+) where {SType<:Union{Bra,Ket,Operator}} = plot_fock_distribution(makeVal(library), ρ; kwargs...)
+
+plot_fock_distribution(::Val{T}, ρ::QuantumObject{SType}; kwargs...) where {T,SType<:Union{Bra,Ket,Operator}} =
+ throw(ArgumentError("The specified plotting library $T is not available. Try running `using $T` first."))
+
+@doc raw"""
+ Bloch(kwargs...)
+
+A structure representing a Bloch sphere visualization for quantum states. Available keyword arguments are listed in the following fields.
+
+# Fields:
+
+## Data storage
+- `points::Vector{Matrix{Float64}}`: Points to plot on the Bloch sphere (3D coordinates)
+- `vectors::Vector{Vector{Float64}}}`: Vectors to plot on the Bloch sphere
+- `lines::Vector{Tuple{Vector{Vector{Float64}},String}}`: Lines to draw on the sphere with each line given as `([start_pt, end_pt], line_format)`
+- `arcs::Vector{Vector{Vector{Float64}}}}`: Arcs to draw on the sphere
+
+## Style properties
+
+- `font_color::String`: Color of axis labels and text. Default: `"black"`
+- `font_size::Int`: Font size for labels. Default: `20`
+- `frame_alpha::Float64`: Transparency of the wire frame. Default: `0.2`
+- `frame_color::String`: Color of the wire frame. Default: `"gray"`
+- `frame_width::Float64` : Width of wire frame. Default: `1.0`
+
+## Point properties
+
+- `point_default_color::Vector{String}}`: Default color cycle for points. Default: `["blue", "red", "green", "#CC6600"]`
+- `point_color::Vector{String}}`: List of colors for Bloch point markers to cycle through. Default: `Union{Nothing,String}[]`
+- `point_marker::Vector{Symbol}}`: List of point marker shapes to cycle through. Default: `[:circle, :rect, :diamond, :utriangle]`
+- `point_size::Vector{Int}}`: List of point marker sizes (not all markers look the same size when plotted). Default: `[5.5, 6.2, 6.5, 7.5]`
+- `point_style::Vector{Symbol}}`: List of marker styles. Default: `Symbol[]`
+- `point_alpha::Vector{Float64}}`: List of marker transparencies. Default: `Float64[]`
+
+## Sphere properties
+
+- `sphere_color::String`: Color of Bloch sphere surface. Default: `"#FFDDDD"`
+- `sphere_alpha::Float64`: Transparency of sphere surface. Default: `0.2`
+
+## Vector properties
+
+- `vector_color::Vector{String}`: Colors for vectors. Default: `["green", "#CC6600", "blue", "red"]`
+- `vector_width::Float64`: Width of vectors. Default: `0.02`
+- `vector_tiplength::Float64`: Length of vector arrow head. Default: `0.08`
+- `vector_tipradius::Float64`: Radius of vector arrow head. Default: `0.05`
+
+## Layout properties
+
+- `view::Vector{Int}`: Azimuthal and elevation viewing angles in degrees. Default: `[30, 30]`
+
+## Label properties
+
+- `xlabel::Vector{AbstractString}`: Labels for x-axis. Default: `[L"x", ""]`
+- `xlpos::Vector{Float64}`: Positions of x-axis labels. Default: `[1.2, -1.2]`
+- `ylabel::Vector{AbstractString}`: Labels for y-axis. Default: `[L"y", ""]`
+- `ylpos::Vector{Float64}`: Positions of y-axis labels. Default: `[1.2, -1.2]`
+- `zlabel::Vector{AbstractString}`: Labels for z-axis. Default: `[L"|0\rangle", L"|1\rangle"]`
+- `zlpos::Vector{Float64}`: Positions of z-axis labels. Default: `[1.2, -1.2]`
+"""
+@kwdef mutable struct Bloch
+ points::Vector{Matrix{Float64}} = Vector{Matrix{Float64}}()
+ vectors::Vector{Vector{Float64}} = Vector{Vector{Float64}}()
+ lines::Vector{Tuple{Vector{Vector{Float64}},String}} = Vector{Tuple{Vector{Vector{Float64}},String}}()
+ arcs::Vector{Vector{Vector{Float64}}} = Vector{Vector{Vector{Float64}}}()
+ font_color::String = "black"
+ font_size::Int = 20
+ frame_alpha::Float64 = 0.2
+ frame_color::String = "gray"
+ frame_width::Float64 = 1.0
+ point_default_color::Vector{String} = ["blue", "red", "green", "#CC6600"]
+ point_color::Vector{Union{Nothing,String}} = Union{Nothing,String}[]
+ point_marker::Vector{Symbol} = [:circle, :rect, :diamond, :utriangle]
+ point_size::Vector{Float64} = [5.5, 6.2, 6.5, 7.5]
+ point_style::Vector{Symbol} = Symbol[]
+ point_alpha::Vector{Float64} = Float64[]
+ sphere_alpha::Float64 = 0.2
+ sphere_color::String = "#FFDDDD"
+ vector_color::Vector{String} = ["green", "#CC6600", "blue", "red"]
+ vector_width::Float64 = 0.02
+ vector_tiplength::Float64 = 0.08
+ vector_tipradius::Float64 = 0.05
+ view::Vector{Int} = [30, 30]
+ xlabel::Vector{AbstractString} = [L"x", ""]
+ xlpos::Vector{Float64} = [1.2, -1.2]
+ ylabel::Vector{AbstractString} = [L"y", ""]
+ ylpos::Vector{Float64} = [1.2, -1.2]
+ zlabel::Vector{AbstractString} = [L"|0\rangle", L"|1\rangle"]
+ zlpos::Vector{Float64} = [1.2, -1.2]
+end
+
+const BLOCH_DATA_FIELDS = (:points, :vectors, :lines, :arcs)
+function Base.show(io::IO, b::Bloch)
+ # To align the output and make it easier to read
+ # we use rpad `17` and `19` for Bloch sphere data and properties, respectively
+ # 17 is the length of string: `Number of vectors`
+ # 19 is the length of string: `point_default_color`
+ println(io, "Bloch Sphere\n")
+ println(io, "data:")
+ println(io, "-----")
+ map(n -> println(io, rpad("Number of $n", 17, " "), " = ", length(getfield(b, n))), BLOCH_DATA_FIELDS)
+ println(io, "")
+ println(io, "properties:")
+ println(io, "-----------")
+ map(n -> (n ∉ BLOCH_DATA_FIELDS) && (println(io, rpad("$n", 19, " "), " = ", getfield(b, n))), fieldnames(Bloch))
+ return nothing
+end
+
+@doc raw"""
+ add_vectors!(b::Bloch, vec::Vector{<:Real})
+
+Add a single normalized vector to the Bloch sphere visualization.
+
+# Arguments
+- `b::Bloch`: The Bloch sphere object to modify
+- `vec::Vector{<:Real}`: A 3D vector to add (will be normalized)
+- `vecs::Vector{<:Vector{<:Real}}}`: List of 3D vectors to add (each will be normalized)
+
+# Example
+```jldoctest
+julia> b = Bloch();
+
+julia> add_vectors!(b, [1, 0, 0])
+1-element Vector{Vector{Float64}}:
+ [1.0, 0.0, 0.0]
+```
+
+We can also add multiple normalized vectors to the Bloch sphere visualization.
+
+```jldoctest
+julia> b = Bloch();
+
+julia> add_vectors!(b, [[1, 0, 0], [0, 1, 0]])
+2-element Vector{Vector{Float64}}:
+ [1.0, 0.0, 0.0]
+ [0.0, 1.0, 0.0]
+```
+"""
+add_vectors!(b::Bloch, vec::Vector{<:Real}) = push!(b.vectors, convert(Vector{Float64}, vec))
+add_vectors!(b::Bloch, vecs::Vector{<:Vector{<:Real}}) = append!(b.vectors, [convert(Vector{Float64}, v) for v in vecs])
+
+@doc raw"""
+ add_points!(b::Bloch, pnt::Vector{<:Real}; meth::Symbol = :s, color = "blue", alpha = 1.0)
+
+Add a single point to the Bloch sphere visualization.
+
+# Arguments
+- `b::Bloch`: The Bloch sphere object to modify
+- `pnt::Vector{<:Real}`: A 3D point to add
+- `meth::Symbol=:s`: Display method (`:s` for single point, `:m` for multiple, `:l` for line)
+- `color`: Color of the point (defaults to first default color if nothing)
+- `alpha=1.0`: Transparency (`1.0` means opaque and `0.0` means transparent)
+"""
+function add_points!(b::Bloch, pnt::Vector{<:Real}; meth::Symbol = :s, color = nothing, alpha = 1.0)
+ return add_points!(b, reshape(pnt, 3, 1); meth, color, alpha)
+end
+function add_points!(b::Bloch, pnts::Vector{<:Vector{<:Real}}; meth::Symbol = :s, color = nothing, alpha = 1.0)
+ return add_points!(b, Matrix(hcat(pnts...)'); meth, color, alpha)
+end
+
+@doc raw"""
+ add_points!(b::Bloch, pnts::Matrix{<:Real}; meth::Symbol = :s, color = nothing, alpha = 1.0)
+
+Add multiple points to the Bloch sphere visualization.
+
+# Arguments
+
+- `b::Bloch`: The Bloch sphere object to modify
+- `pnts::Matrix{<:Real}`: `3×N` matrix of points (each column is a point)
+- `meth::Symbol=:s`: Display method (`:s` for single point, `:m` for multiple, `:l` for line)
+- `color`: Color of the points (defaults to first default color if `nothing`)
+- `alpha=1.0`: Transparency (`1.0` means opaque and `0.0` means transparent)
+```
+"""
+function add_points!(
+ b::Bloch,
+ pnts::AbstractMatrix{<:Real};
+ meth::Symbol = :s,
+ color::Union{Nothing,String} = nothing,
+ alpha::Float64 = 1.0,
+)
+ (size(pnts, 1) == 3) || throw(ArgumentError("Points must be a 3×N matrix where each column is [x; y; z]"))
+ (meth in (:s, :m, :l)) || throw(ArgumentError("`meth` must be :s, :m, or :l"))
+
+ push!(b.points, convert(Matrix{Float64}, pnts))
+ push!(b.point_style, meth)
+ push!(b.point_alpha, alpha)
+ push!(b.point_color, color)
+ return nothing
+end
+
+@doc raw"""
+ add_line!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}; fmt = "k", kwargs...)
+
+Add a line between two points on the Bloch sphere.
+
+# Arguments
+- `b::Bloch`: The Bloch sphere object to modify
+- `p1::Vector{<:Real}`: First 3D point
+- `p2::Vector{<:Real}`: Second 3D point
+- `fmt="k"`: Line format string (matplotlib style)
+"""
+function add_line!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}; fmt = "k")
+ (length(p1) != 3 || length(p2) != 3) && throw(ArgumentError("Points must be 3D vectors"))
+ x = [p1[1], p2[1]]
+ y = [p1[2], p2[2]]
+ z = [p1[3], p2[3]]
+ push!(b.lines, ([x, y, z], fmt))
+ return b
+end
+
+@doc raw"""
+ add_line!(
+ b::Bloch,
+ start_point::QuantumObject,
+ end_point::QuantumObject;
+ fmt = "k"
+ )
+
+Add a line between two quantum states on the Bloch sphere visualization.
+
+# Arguments
+
+- `b::Bloch`: The Bloch sphere object to modify.
+- `start_point::QuantumObject`: The starting quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+- `end_point::QuantumObject`: The ending quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+- `fmt::String="k"`: (optional) A format string specifying the line style and color (default is black `"k"`).
+
+# Description
+
+This function converts the given quantum states into their Bloch vector representations and adds a line between these two points on the Bloch sphere visualization.
+
+# Example
+
+```julia
+b = Bloch()
+ψ₁ = normalize(basis(2, 0) + basis(2, 1))
+ψ₂ = normalize(basis(2, 0) - im * basis(2, 1))
+add_line!(b, ψ₁, ψ₂; fmt = "r--")
+```
+"""
+function add_line!(
+ b::Bloch,
+ start_point::QuantumObject{OpType1},
+ end_point::QuantumObject{OpType2};
+ fmt = "k",
+) where {OpType1<:Union{Ket,Bra,Operator},OpType2<:Union{Ket,Bra,Operator}}
+ coords1 = _state_to_bloch(start_point)
+ coords2 = _state_to_bloch(end_point)
+ return add_line!(b, coords1, coords2; fmt = fmt)
+end
+
+@doc raw"""
+ add_arc!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}, p3::Vector{<:Real})
+
+Add a circular arc through three points on the Bloch sphere.
+
+# Arguments
+
+- `b::Bloch`: The Bloch sphere object to modify
+- `p1::Vector{<:Real}`: Starting 3D point
+- `p2::Vector{<:Real}`: [Optional] Middle 3D point
+- `p3::Vector{<:Real}`: Ending 3D point
+
+# Examples
+
+```jldoctest
+julia> b = Bloch();
+
+julia> add_arc!(b, [1, 0, 0], [0, 1, 0], [0, 0, 1])
+1-element Vector{Vector{Vector{Float64}}}:
+ [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
+```
+"""
+function add_arc!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real})
+ (length(p1) != 3 || length(p2) != 3) && throw(ArgumentError("Points must be 3D vectors"))
+ return push!(b.arcs, [convert(Vector{Float64}, p1), convert(Vector{Float64}, p2)])
+end
+function add_arc!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}, p3::Vector{<:Real})
+ (length(p1) != 3 || length(p2) != 3 || length(p3) != 3) && throw(ArgumentError("Points must be 3D vectors"))
+ return push!(b.arcs, [convert(Vector{Float64}, p1), convert(Vector{Float64}, p2), convert(Vector{Float64}, p3)])
+end
+
+@doc raw"""
+ add_arc!(
+ b::Bloch,
+ start_point::QuantumObject,
+ middle_point::QuantumObject,
+ end_point::QuantumObject
+ )
+
+Add a circular arc through three points on the Bloch sphere.
+
+# Arguments
+
+- `b::Bloch`: The Bloch sphere object to modify.
+- `start_point::QuantumObject`: The starting quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+- `middle_point::QuantumObject`: [Optional] The middle quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+- `end_point::QuantumObject`: The ending quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+
+# Description
+
+This function converts the given quantum states into their Bloch vector representations and adds a arc between these two (or three) points on the Bloch sphere visualization.
+"""
+function add_arc!(
+ b::Bloch,
+ start_point::QuantumObject{OpType1},
+ end_point::QuantumObject{OpType2},
+) where {OpType1<:Union{Ket,Bra,Operator},OpType2<:Union{Ket,Bra,Operator}}
+ coords1 = _state_to_bloch(start_point)
+ coords2 = _state_to_bloch(end_point)
+ return add_arc!(b, coords1, coords2)
+end
+function add_arc!(
+ b::Bloch,
+ start_point::QuantumObject{OpType1},
+ middle_point::QuantumObject{OpType2},
+ end_point::QuantumObject{OpType3},
+) where {OpType1<:Union{Ket,Bra,Operator},OpType2<:Union{Ket,Bra,Operator},OpType3<:Union{Ket,Bra,Operator}}
+ coords1 = _state_to_bloch(start_point)
+ coords2 = _state_to_bloch(middle_point)
+ coords3 = _state_to_bloch(end_point)
+ return add_arc!(b, coords1, coords2, coords3)
+end
+
+@doc raw"""
+ add_states!(b::Bloch, states::Vector{QuantumObject}; kind::Symbol = :vector, kwargs...)
+
+Add one or more quantum states to the Bloch sphere visualization by converting them into Bloch vectors.
+
+# Arguments
+- `b::Bloch`: The Bloch sphere object to modify
+- `states::Vector{QuantumObject}`: One or more quantum states ([`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref))
+- `kind::Symbol`: Type of object to plot (can be either `:vector` or `:point`). Default: `:vector`
+
+# Example
+
+```julia
+x = basis(2, 0) + basis(2, 1);
+y = basis(2, 0) + im * basis(2, 1);
+z = basis(2, 0);
+b = Bloch();
+add_states!(b, [x, y, z])
+```
+"""
+function add_states!(b::Bloch, states::Vector{<:QuantumObject}; kind::Symbol = :vector, kwargs...)
+ vecs = map(state -> _state_to_bloch(state), states)
+ if kind == :vector
+ add_vectors!(b, vecs)
+ elseif kind == :point
+ add_points!(b, hcat(vecs...); kwargs...)
+ else
+ throw(ArgumentError("Invalid kind = :$kind"))
+ end
+ return nothing
+end
+add_states!(b::Bloch, state::QuantumObject; kind::Symbol = :vector, kwargs...) =
+ add_states!(b, [state], kind = kind, kwargs...)
+
+_state_to_bloch(state::QuantumObject{Ket}) = _ket_to_bloch(state)
+_state_to_bloch(state::QuantumObject{Bra}) = _ket_to_bloch(state')
+_state_to_bloch(state::QuantumObject{Operator}) = _dm_to_bloch(state)
+
+raw"""
+ _ket_to_bloch(state::QuantumObject{Ket}) -> Vector{Float64}
+
+Convert a pure qubit state (`Ket`) to its Bloch vector representation.
+
+If the state is not normalized, it is automatically normalized before conversion.
+
+# Arguments
+- `state`: A `Ket` representing a pure quantum state.
+
+# Returns
+A 3-element `Vector{Float64}` representing the Bloch vector `[x, y, z]`.
+
+# Throws
+- `ArgumentError` if the state dimension is not 2.
+"""
+function _ket_to_bloch(state::QuantumObject{Ket})
+ (size(state) == (2,)) ||
+ throw(ArgumentError("Bloch sphere visualization is only supported for qubit states (2-level systems)"))
+
+ state_norm = norm(state)
+ if !isapprox(state_norm, 1.0, atol = 1e-6)
+ @warn "State is not normalized. Normalizing before Bloch vector conversion."
+ ψ = state.data / state_norm
+ else
+ ψ = state.data
+ end
+
+ c = conj(ψ[1]) * ψ[2]
+ x = 2 * real(c)
+ y = 2 * imag(c)
+ z = abs2(ψ[1]) - abs2(ψ[2])
+ return [x, y, z]
+end
+
+raw"""
+ _dm_to_bloch(ρ::QuantumObject{Operator}) -> Vector{Float64}
+
+Convert a qubit density matrix (`Operator`) to its Bloch vector representation.
+
+This function assumes the input is Hermitian. If the density matrix is not Hermitian, a warning is issued.
+
+# Arguments
+- `ρ`: A density matrix (`Operator`) representing a mixed or pure quantum state.
+
+# Returns
+A 3-element `Vector{Float64}` representing the Bloch vector `[x, y, z]`.
+
+# Throws
+- `ArgumentError` if the matrix dimension is not 2.
+"""
+function _dm_to_bloch(ρ::QuantumObject{Operator})
+ (size(ρ) == (2, 2)) ||
+ throw(ArgumentError("Bloch sphere visualization is only supported for qubit states (2-level systems)"))
+
+ ishermitian(ρ) || (@warn "Density matrix is not Hermitian. Results may not be meaningful.")
+
+ state_norm = norm(ρ)
+ if !isapprox(state_norm, 1.0, atol = 1e-6)
+ @warn "State is not normalized. Normalizing before Bloch vector conversion."
+ ρ2 = ρ / state_norm
+ else
+ ρ2 = ρ
+ end
+ x = real(ρ2[1, 2] + ρ2[2, 1])
+ y = imag(ρ2[2, 1] - ρ2[1, 2])
+ z = real(ρ2[1, 1] - ρ2[2, 2])
+ return [x, y, z]
+end
+
+@doc raw"""
+ clear!(b::Bloch)
+
+Clear all graphical elements (points, vectors, lines, arcs) from the given [`Bloch`](@ref) sphere object `b`.
+
+# Arguments
+
+- `b::Bloch`: The Bloch sphere instance whose contents will be cleared.
+
+# Returns
+
+- The updated `Bloch` object `b` with all points, vectors, lines, and arcs removed.
+"""
+function clear!(b::Bloch)
+ empty!(b.points)
+ empty!(b.point_color)
+ empty!(b.point_style)
+ empty!(b.point_alpha)
+ empty!(b.vectors)
+ empty!(b.lines)
+ empty!(b.arcs)
+ return b
+end
+
+@doc raw"""
+ render(b::Bloch; location=nothing)
+
+Render the Bloch sphere visualization from the given [`Bloch`](@ref) object `b`.
+
+# Arguments
+
+- `b::Bloch`: The Bloch sphere object containing states, vectors, and settings to visualize.
+- `location::Union{GridPosition,Nothing}`: The location of the plot in the layout. If `nothing`, the plot is created in a new figure. Default is `nothing`.
+
+# Returns
+
+- A tuple `(fig, lscene)` where `fig` is the figure object and `lscene` is the `LScene` object used for plotting. These can be further manipulated or saved by the user.
+"""
+function render end
+
+@doc raw"""
+ plot_bloch(
+ state::QuantumObject;
+ library::Union{Symbol, Val} = :Makie,
+ kwargs...
+ )
+
+Plot the state of a two-level quantum system on the Bloch sphere.
+
+The `library` keyword argument specifies the plotting backend to use. The default is `:Makie`, which uses the [`Makie.jl`](https://github.com/MakieOrg/Makie.jl) plotting library. This function internally dispatches to a type-stable version based on `Val(:Makie)` or other plotting backends.
+
+# Arguments
+- `state::QuantumObject`: The quantum state to be visualized. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+- `library::Union{Val,Symbol}`: The plotting library to use. Default is `Val(:Makie)`.
+- `kwargs...`: Additional keyword arguments passed to the specific plotting implementation.
+
+!!! note "Import library first"
+ The plotting backend library must be imported before use.
+
+!!! warning "Beware of type-stability!"
+ For improved performance and type-stability, prefer passing `Val(:Makie)` instead of `:Makie`. See [Performance Tips](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) for details.
+"""
+plot_bloch(
+ state::QuantumObject{OpType};
+ library::Union{Symbol,Val} = Val(:Makie),
+ kwargs...,
+) where {OpType<:Union{Ket,Bra,Operator}} = plot_bloch(makeVal(library), state; kwargs...)
+plot_bloch(::Val{T}, state::QuantumObject{OpType}; kwargs...) where {T,OpType<:Union{Ket,Bra,Operator}} =
+ throw(ArgumentError("The specified plotting library $T is not available. Try running `using $T` first."))
diff --git a/src/wigner.jl b/src/wigner.jl
index 466cbe376..b4954a9a4 100644
--- a/src/wigner.jl
+++ b/src/wigner.jl
@@ -12,34 +12,70 @@ end
WignerLaguerre(; parallel = false, tol = 1e-14) = WignerLaguerre(parallel, tol)
@doc raw"""
- wigner(state::QuantumObject, xvec::AbstractVector, yvec::AbstractVector; g::Real=√2,
- solver::WignerSolver=WignerLaguerre())
-
-Generates the [Wigner quasipropability distribution](https://en.wikipedia.org/wiki/Wigner_quasiprobability_distribution)
-of `state` at points `xvec + 1im * yvec`. The `g` parameter is a scaling factor related to the value of ``\hbar`` in the
-commutation relation ``[x, y] = i \hbar`` via ``\hbar=2/g^2`` giving the default value ``\hbar=1``.
-
-The `solver` parameter can be either `WignerLaguerre()` or `WignerClenshaw()`. The former uses the Laguerre polynomial
-expansion of the Wigner function, while the latter uses the Clenshaw algorithm. The Laguerre expansion is faster for
-sparse matrices, while the Clenshaw algorithm is faster for dense matrices. The `WignerLaguerre` solver has an optional
-`parallel` parameter which defaults to `true` and uses multithreading to speed up the calculation.
+ wigner(
+ state::QuantumObject{OpType},
+ xvec::AbstractVector,
+ yvec::AbstractVector;
+ g::Real = √2,
+ method::WignerSolver = WignerClenshaw(),
+ )
+
+Generates the [Wigner quasipropability distribution](https://en.wikipedia.org/wiki/Wigner_quasiprobability_distribution) of `state` at points `xvec + 1im * yvec` in phase space. The `g` parameter is a scaling factor related to the value of ``\hbar`` in the commutation relation ``[x, y] = i \hbar`` via ``\hbar=2/g^2`` giving the default value ``\hbar=1``.
+
+The `method` parameter can be either `WignerLaguerre()` or `WignerClenshaw()`. The former uses the Laguerre polynomial expansion of the Wigner function, while the latter uses the Clenshaw algorithm. The Laguerre expansion is faster for sparse matrices, while the Clenshaw algorithm is faster for dense matrices. The `WignerLaguerre` method has an optional `parallel` parameter which defaults to `true` and uses multithreading to speed up the calculation.
+
+# Arguments
+- `state::QuantumObject`: The quantum state for which the Wigner function is calculated. It can be either a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref).
+- `xvec::AbstractVector`: The x-coordinates of the phase space grid.
+- `yvec::AbstractVector`: The y-coordinates of the phase space grid.
+- `g::Real`: The scaling factor related to the value of ``\hbar`` in the commutation relation ``[x, y] = i \hbar`` via ``\hbar=2/g^2``.
+- `method::WignerSolver`: The method used to calculate the Wigner function. It can be either `WignerLaguerre()` or `WignerClenshaw()`, with `WignerClenshaw()` as default. The `WignerLaguerre` method has the optional `parallel` and `tol` parameters, with default values `true` and `1e-14`, respectively.
+
+# Returns
+- `W::Matrix`: The Wigner function of the state at the points `xvec + 1im * yvec` in phase space.
+
+# Example
+```jldoctest wigner
+julia> ψ = fock(10, 0) + fock(10, 1) |> normalize
+
+Quantum Object: type=Ket() dims=[10] size=(10,)
+10-element Vector{ComplexF64}:
+ 0.7071067811865475 + 0.0im
+ 0.7071067811865475 + 0.0im
+ 0.0 + 0.0im
+ 0.0 + 0.0im
+ 0.0 + 0.0im
+ 0.0 + 0.0im
+ 0.0 + 0.0im
+ 0.0 + 0.0im
+ 0.0 + 0.0im
+ 0.0 + 0.0im
+
+julia> xvec = range(-5, 5, 200)
+-5.0:0.05025125628140704:5.0
+
+julia> wig = wigner(ψ, xvec, xvec);
+```
+
+or taking advantage of the parallel computation of the `WignerLaguerre` method
+
+```jldoctest wigner
+julia> ρ = ket2dm(ψ) |> to_sparse;
+
+julia> wig = wigner(ρ, xvec, xvec, method=WignerLaguerre(parallel=true));
+
+```
"""
function wigner(
- state::QuantumObject{<:AbstractArray{T},OpType},
+ state::QuantumObject{OpType},
xvec::AbstractVector,
yvec::AbstractVector;
g::Real = √2,
- solver::MySolver = WignerLaguerre(),
-) where {T,OpType<:Union{BraQuantumObject,KetQuantumObject,OperatorQuantumObject},MySolver<:WignerSolver}
- if isket(state)
- ρ = (state * state').data
- elseif isbra(state)
- ρ = (state' * state).data
- else
- ρ = state.data
- end
+ method::WignerSolver = WignerClenshaw(),
+) where {OpType<:Union{Bra,Ket,Operator}}
+ ρ = ket2dm(state).data
- return _wigner(ρ, xvec, yvec, g, solver)
+ return _wigner(ρ, xvec, yvec, g, method)
end
function _wigner(
@@ -47,7 +83,7 @@ function _wigner(
xvec::AbstractVector{T},
yvec::AbstractVector{T},
g::Real,
- solver::WignerLaguerre,
+ method::WignerLaguerre,
) where {T<:BlasFloat}
g = convert(T, g)
X, Y = meshgrid(xvec, yvec)
@@ -55,7 +91,7 @@ function _wigner(
W = similar(A, T)
W .= 0
- return _wigner_laguerre(ρ, A, W, g, solver)
+ return _wigner_laguerre(ρ, A, W, g, method)
end
function _wigner(
@@ -63,7 +99,7 @@ function _wigner(
xvec::AbstractVector{T},
yvec::AbstractVector{T},
g::Real,
- solver::WignerClenshaw,
+ method::WignerClenshaw,
) where {T1<:BlasFloat,T<:BlasFloat}
g = convert(T, g)
M = size(ρ, 1)
@@ -90,11 +126,11 @@ function _wigner(
return @. real(W) * exp(-B / 2) * g^2 / 2 / π
end
-function _wigner_laguerre(ρ::AbstractSparseArray, A::AbstractArray, W::AbstractArray, g::Real, solver::WignerLaguerre)
+function _wigner_laguerre(ρ::AbstractSparseArray, A::AbstractArray, W::AbstractArray, g::Real, method::WignerLaguerre)
rows, cols, vals = findnz(ρ)
B = @. 4 * abs2(A)
- if solver.parallel
+ if method.parallel
iter = filter(x -> x[2] >= x[1], collect(zip(rows, cols, vals)))
Wtot = similar(B, size(B)..., length(iter))
Threads.@threads for i in eachindex(iter)
@@ -122,18 +158,18 @@ function _wigner_laguerre(ρ::AbstractSparseArray, A::AbstractArray, W::Abstract
return @. W * g^2 * exp(-B / 2) / 2 / π
end
-function _wigner_laguerre(ρ::AbstractArray, A::AbstractArray, W::AbstractArray, g::Real, solver::WignerLaguerre)
- tol = solver.tol
+function _wigner_laguerre(ρ::AbstractArray, A::AbstractArray, W::AbstractArray, g::Real, method::WignerLaguerre)
+ tol = method.tol
M = size(ρ, 1)
B = @. 4 * abs2(A)
- if solver.parallel
+ if method.parallel
throw(ArgumentError("Parallel version is not implemented for dense matrices"))
else
- for m in 0:M-1
+ for m in 0:(M-1)
ρmn = ρ[m+1, m+1]
abs(ρmn) > tol && (@. W += real(ρmn * (-1)^m * _genlaguerre(m, 0, B)))
- for n in m+1:M-1
+ for n in (m+1):(M-1)
ρmn = ρ[m+1, n+1]
# Γ_mn = sqrt(gamma(m+1) / gamma(n+1))
Γ_mn = sqrt(exp(loggamma(m + 1) - loggamma(n + 1))) # Is this a good trick?
@@ -161,7 +197,7 @@ function _genlaguerre(n::Int, α::Number, x::T) where {T<:BlasFloat}
α = convert(T, α)
p0, p1 = one(T), -x + (α + 1)
n == 0 && return p0
- for k in 1:n-1
+ for k in 1:(n-1)
p1, p0 = ((2k + α + 1) / (k + 1) - x / (k + 1)) * p1 - (k + α) / (k + 1) * p0, p1
end
return p1
diff --git a/test/Project.toml b/test/Project.toml
new file mode 100644
index 000000000..b783960dd
--- /dev/null
+++ b/test/Project.toml
@@ -0,0 +1,15 @@
+[deps]
+LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
+Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961"
+SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
+Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
+
+[compat]
+Pkg = "1"
+Test = "1"
+TestItemRunner = "1"
diff --git a/test/permutation.jl b/test/core-test/block_diagonal_form.jl
similarity index 62%
rename from test/permutation.jl
rename to test/core-test/block_diagonal_form.jl
index 6f0892548..201b63ff2 100644
--- a/test/permutation.jl
+++ b/test/core-test/block_diagonal_form.jl
@@ -1,4 +1,4 @@
-@testset "Permutation" begin
+@testitem "Block Diagonal Form" begin
# Block Diagonal Form
N = 20
Δ = 0
@@ -16,15 +16,17 @@
c_ops = [√(κ2) * a^2, √(κϕ) * ad * a]
L = liouvillian(H, c_ops)
- P, L_bd, block_sizes = bdf(L)
- blocks_list, block_indices = get_bdf_blocks(L_bd, block_sizes)
+ bdf = block_diagonal_form(L)
+ L_bd = bdf.B
+ block_sizes = bdf.block_sizes
+ blocks = bdf.blocks
+
@test size(L_bd) == size(L)
@test length(block_sizes) == 4
- @test length(blocks_list) == 4
- @test length(block_indices) == 4
+ @test length(blocks) == 4
@test sum(block_sizes .== 100) == 4
- @testset "Type Inference (bdf)" begin
- @inferred bdf(L)
+ @testset "Type Inference (block_diagonal_form)" begin
+ @inferred block_diagonal_form(L)
end
end
diff --git a/test/core-test/brmesolve.jl b/test/core-test/brmesolve.jl
new file mode 100644
index 000000000..2ac87a0b1
--- /dev/null
+++ b/test/core-test/brmesolve.jl
@@ -0,0 +1,116 @@
+@testitem "Bloch-Redfield tensor sec_cutoff" begin
+ N = 5
+ H = num(N)
+ a = destroy(N)
+ A_op = a+a'
+ spectra(x) = (x>0) * 0.5
+ for sec_cutoff in [0, 0.1, 1, 3, -1]
+ R = bloch_redfield_tensor(H, ((A_op, spectra),), [a^2], sec_cutoff = sec_cutoff, fock_basis = Val(true))
+ R_eig, evecs =
+ bloch_redfield_tensor(H, ((A_op, spectra),), [a^2], sec_cutoff = sec_cutoff, fock_basis = Val(false))
+ @test isa(R, QuantumObject)
+ @test isa(R_eig, QuantumObject)
+ @test isa(evecs, QuantumObject)
+
+ state = rand_dm(N) |> mat2vec
+ fock_computed = R * state
+ eig_computed = (sprepost(evecs, evecs') * R_eig * sprepost(evecs', evecs)) * state
+ @test isapprox(fock_computed, eig_computed, atol = 1e-15)
+ end
+end
+
+@testitem "Compare brterm and Lindblad" begin
+ N = 5
+ H = num(N)
+ a = destroy(N) + destroy(N)^2/2
+ A_op = a+a'
+ spectra(x) = x>0
+
+ # this test applies for limited cutoff
+ lindblad = lindblad_dissipator(a)
+ computation = brterm(H, A_op, spectra, sec_cutoff = 1.5, fock_basis = Val(true))
+ @test isapprox(lindblad, computation, atol = 1e-15)
+end
+
+@testitem "brterm basis" begin
+ N = 5
+ H = num(N)
+ a = destroy(N) + destroy(N)^2/2
+ A_op = a+a'
+ spectra(x) = x>0
+ for sec_cutoff in [0, 0.1, 1, 3, -1]
+ R = brterm(H, A_op, spectra, sec_cutoff = sec_cutoff, fock_basis = Val(true))
+ R_eig, evecs = brterm(H, A_op, spectra, sec_cutoff = sec_cutoff, fock_basis = Val(false))
+ @test isa(R, QuantumObject)
+ @test isa(R_eig, QuantumObject)
+ @test isa(evecs, QuantumObject)
+
+ state = rand_dm(N) |> mat2vec
+ fock_computed = R * state
+ eig_computed = (sprepost(evecs, evecs') * R_eig * sprepost(evecs', evecs)) * state
+ @test isapprox(fock_computed, eig_computed, atol = 1e-15)
+ end;
+end
+
+@testitem "brterm sprectra function" begin
+ f(x) = exp(x)/10
+ function g(x)
+ nbar = n_thermal(abs(x), 1)
+ if x > 0
+ return nbar
+ elseif x < 0
+ return 1 + nbar
+ else
+ return 0.0
+ end
+ end
+
+ spectra_list = [
+ x -> (x>0), # positive frequency filter
+ x -> one(x), # no filter
+ f, # smooth filter
+ g, # thermal field
+ ]
+
+ N = 5
+ H = num(N)
+ a = destroy(N) + destroy(N)^2/2
+ A_op = a+a'
+ for spectra in spectra_list
+ R = brterm(H, A_op, spectra, sec_cutoff = 0.1, fock_basis = Val(true))
+ R_eig, evecs = brterm(H, A_op, spectra, sec_cutoff = 0.1, fock_basis = Val(false))
+ @test isa(R, QuantumObject)
+ @test isa(R_eig, QuantumObject)
+ @test isa(evecs, QuantumObject)
+
+ state = rand_dm(N) |> mat2vec
+ fock_computed = R * state
+ eig_computed = (sprepost(evecs, evecs') * R_eig * sprepost(evecs', evecs)) * state
+ @test isapprox(fock_computed, eig_computed, atol = 1e-15)
+ end
+end
+
+@testitem "simple qubit system" begin
+ pauli_vectors = [sigmax(), sigmay(), sigmaz()]
+ γ = 0.25
+ spectra(x) = γ * (x>=0)
+ _m_c_op = √γ * sigmam()
+ _z_c_op = √γ * sigmaz()
+ _x_a_op = (sigmax(), spectra)
+
+ arg_sets = [[[_m_c_op], [], [_x_a_op]], [[_m_c_op], [_m_c_op], []], [[_m_c_op, _z_c_op], [_z_c_op], [_x_a_op]]]
+
+ δ = 0
+ ϵ = 0.5 * 2π
+ e_ops = pauli_vectors
+ H = δ * 0.5 * sigmax() + ϵ * 0.5 * sigmaz()
+ ψ0 = unit(2basis(2, 0) + basis(2, 1))
+ tlist = LinRange(0, 10, 100)
+
+ for (me_c_ops, brme_c_ops, brme_a_ops) in arg_sets
+ me = mesolve(H, ψ0, tlist, me_c_ops, e_ops = e_ops, progress_bar = Val(false))
+ brme = brmesolve(H, ψ0, tlist, brme_a_ops, brme_c_ops, e_ops = e_ops, progress_bar = Val(false))
+
+ @test all(me.expect .== brme.expect)
+ end
+end
diff --git a/test/core-test/code-quality/Project.toml b/test/core-test/code-quality/Project.toml
new file mode 100644
index 000000000..6a0832dc2
--- /dev/null
+++ b/test/core-test/code-quality/Project.toml
@@ -0,0 +1,9 @@
+[deps]
+Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
+JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
+QuantumToolbox = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab"
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+
+[compat]
+Aqua = "0.8"
+JET = "0.9, 0.10"
diff --git a/test/code_quality.jl b/test/core-test/code-quality/code_quality.jl
similarity index 94%
rename from test/code_quality.jl
rename to test/core-test/code-quality/code_quality.jl
index 724639a31..5effb97d1 100644
--- a/test/code_quality.jl
+++ b/test/core-test/code-quality/code_quality.jl
@@ -1,5 +1,3 @@
-using Aqua, JET
-
@testset "Code quality" verbose = true begin
@testset "Aqua.jl" begin
Aqua.test_all(QuantumToolbox; ambiguities = false, unbound_args = false)
diff --git a/test/core-test/correlations_and_spectrum.jl b/test/core-test/correlations_and_spectrum.jl
new file mode 100644
index 000000000..71c875034
--- /dev/null
+++ b/test/core-test/correlations_and_spectrum.jl
@@ -0,0 +1,79 @@
+@testitem "Correlations and Spectrum" begin
+ N = 10
+ Id = qeye(N)
+ a = destroy(N)
+ H = a' * a
+ c_ops = [sqrt(0.1 * (0.01 + 1)) * a, sqrt(0.1 * (0.01)) * a']
+
+ t_l = range(0, 333 * π, length = 1000)
+ corr1 = correlation_2op_1t(H, nothing, t_l, c_ops, a', a; progress_bar = Val(false))
+ corr2 = correlation_3op_1t(H, nothing, t_l, c_ops, Id, a', a; progress_bar = Val(false))
+ ω_l1, spec1 = spectrum_correlation_fft(t_l, corr1)
+
+ ω_l2 = range(0, 3, length = 1000)
+ spec2 = spectrum(H, ω_l2, c_ops, a', a)
+ spec3 = spectrum(H, ω_l2, c_ops, a', a; solver = PseudoInverse())
+ spec4 = spectrum(H, ω_l2, c_ops, a', a; solver = Lanczos())
+
+ spec1 = spec1 ./ maximum(spec1)
+ spec2 = spec2 ./ maximum(spec2)
+ spec3 = spec3 ./ maximum(spec3)
+ spec4 = spec4 ./ maximum(spec4)
+
+ test_func1 = maximum(real.(spec1)) * (0.1 / 2)^2 ./ ((ω_l1 .- 1) .^ 2 .+ (0.1 / 2)^2)
+ test_func2 = maximum(real.(spec2)) * (0.1 / 2)^2 ./ ((ω_l2 .- 1) .^ 2 .+ (0.1 / 2)^2)
+ idxs1 = test_func1 .> 0.05
+ idxs2 = test_func2 .> 0.05
+ @test sum(abs2.(spec1[idxs1] .- test_func1[idxs1])) / sum(abs2.(test_func1[idxs1])) < 0.01
+ @test sum(abs2.(spec2[idxs2] .- test_func2[idxs2])) / sum(abs2.(test_func2[idxs2])) < 0.01
+ @test all(corr1 .≈ corr2)
+ @test all(spec2 .≈ spec3)
+ @test all(spec2 .≈ spec4)
+
+ @testset "Type Inference spectrum" begin
+ @inferred correlation_2op_1t(H, nothing, t_l, c_ops, a', a; progress_bar = Val(false))
+ @inferred spectrum_correlation_fft(t_l, corr1)
+ @inferred spectrum(H, ω_l2, c_ops, a', a)
+ @inferred spectrum(H, ω_l2, c_ops, a', a; solver = PseudoInverse())
+ @inferred spectrum(H, ω_l2, c_ops, a', a; solver = Lanczos())
+ end
+
+ @testset "Verbose mode Lanczos" begin
+ cout = stdout
+ r, w = redirect_stdout()
+ nout = @async read(r, String)
+ spectrum(H, ω_l2, c_ops, a', a; solver = Lanczos(verbose = 2, maxiter = 2, tol = 1e-16));
+ redirect_stdout(cout)
+ close(w)
+ out = fetch(nout)
+ outlines = split(out, '\n', keepempty = false)
+ @test last(outlines) == "spectrum(): Consider increasing maxiter and/or tol"
+ end
+
+ @testset "Orthogonal input vectors Lanczos" begin
+ @test_throws AssertionError spectrum(H, ω_l2, [c_ops[1]], a', a; solver = Lanczos())
+ end
+
+ # tlist and τlist checks
+ t_fft_wrong = [0, 1, 10]
+ t_wrong1 = [1, 2, 3]
+ t_wrong2 = [-1, 0, 1]
+ @test_throws ArgumentError spectrum_correlation_fft(t_fft_wrong, corr1)
+ @test_throws ArgumentError correlation_3op_2t(H, nothing, t_l, t_wrong1, c_ops, Id, a', a)
+ @test_throws ArgumentError correlation_3op_2t(H, nothing, t_l, t_wrong2, c_ops, Id, a', a)
+ @test_throws ArgumentError correlation_3op_2t(H, nothing, t_wrong1, t_l, c_ops, Id, a', a)
+ @test_throws ArgumentError correlation_3op_2t(H, nothing, t_wrong2, t_l, c_ops, Id, a', a)
+ @test_throws ArgumentError correlation_3op_2t(H, nothing, t_wrong1, t_wrong1, c_ops, Id, a', a)
+ @test_throws ArgumentError correlation_3op_2t(H, nothing, t_wrong1, t_wrong2, c_ops, Id, a', a)
+ @test_throws ArgumentError correlation_3op_2t(H, nothing, t_wrong2, t_wrong1, c_ops, Id, a', a)
+ @test_throws ArgumentError correlation_3op_2t(H, nothing, t_wrong2, t_wrong2, c_ops, Id, a', a)
+
+ @testset "Deprecated Errors" begin
+ ρ0 = rand_dm(N)
+ @test_throws ErrorException FFTCorrelation()
+ @test_throws ErrorException correlation_3op_2t(H, ρ0, t_l, t_l, a, a', a, c_ops)
+ @test_throws ErrorException correlation_3op_1t(H, ρ0, t_l, a, a', a, c_ops)
+ @test_throws ErrorException correlation_2op_2t(H, ρ0, t_l, t_l, a', a, c_ops)
+ @test_throws ErrorException correlation_2op_1t(H, ρ0, t_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 97%
rename from test/dynamical-shifted-fock.jl
rename to test/core-test/dynamical-shifted-fock.jl
index b0b529ac9..ff9fd3ffc 100644
--- a/test/dynamical-shifted-fock.jl
+++ b/test/core-test/dynamical-shifted-fock.jl
@@ -1,4 +1,4 @@
-@testset "Dynamical Shifted Fock" begin
+@testitem "Dynamical Shifted Fock" begin
F = 3
Δ = 0.25
κ = 1
@@ -60,7 +60,6 @@
dsf_params,
e_ops = e_ops_dsf,
progress_bar = Val(false),
- n_traj = 500,
)
val_ss = abs2(sol0.expect[1, end])
@test sum(abs2.(sol0.expect[1, :] .- sol_dsf_me.expect[1, :])) / (val_ss * length(tlist)) < 0.1
@@ -140,7 +139,6 @@
dsf_params,
e_ops = e_ops_dsf2,
progress_bar = Val(false),
- n_traj = 500,
)
val_ss = abs2(sol0.expect[1, end])
diff --git a/test/dynamical_fock_dimension_mesolve.jl b/test/core-test/dynamical_fock_dimension_mesolve.jl
similarity index 88%
rename from test/dynamical_fock_dimension_mesolve.jl
rename to test/core-test/dynamical_fock_dimension_mesolve.jl
index 9c6c8a02a..cd213ec00 100644
--- a/test/dynamical_fock_dimension_mesolve.jl
+++ b/test/core-test/dynamical_fock_dimension_mesolve.jl
@@ -1,5 +1,5 @@
### DYNAMICAL FOCK DIMENSION ###
-@testset "Dynamical Fock Dimension" begin
+@testitem "Dynamical Fock Dimension" begin
F, Δ, κ = 5, 0.25, 1
t_l = range(0, 15, length = 100)
@@ -29,9 +29,23 @@
maxdims = [150]
ψ0 = fock(3, 0)
dfd_params = (Δ = Δ, F = F, κ = κ)
- sol = dfd_mesolve(H_dfd0, ψ0, t_l, c_ops_dfd0, maxdims, dfd_params, e_ops = e_ops_dfd0, progress_bar = Val(false))
+ sol = dfd_mesolve(
+ H_dfd0,
+ ψ0,
+ t_l,
+ c_ops_dfd0,
+ maxdims,
+ dfd_params,
+ e_ops = e_ops_dfd0,
+ progress_bar = Val(false),
+ saveat = t_l,
+ )
@test sum(abs.((sol.expect[1, :] .- sol0.expect[1, :]) ./ (sol0.expect[1, :] .+ 1e-16))) < 0.01
+ @test length(sol.states) == length(t_l)
+ @test all(
+ diff(getindex.(QuantumToolbox.dimensions_to_dims.(QuantumToolbox.get_dimensions_to.(sol.states)), 1)) .>= 0,
+ )
######################
diff --git a/test/core-test/eigenvalues_and_operators.jl b/test/core-test/eigenvalues_and_operators.jl
new file mode 100644
index 000000000..e19007a3b
--- /dev/null
+++ b/test/core-test/eigenvalues_and_operators.jl
@@ -0,0 +1,113 @@
+@testitem "Eigenvalues" begin
+ σx = sigmax()
+ result = eigenstates(σx, sparse = false)
+ λd, ψd, Td = result
+ resstring = sprint((t, s) -> show(t, "text/plain", s), result)
+ valstring = sprint((t, s) -> show(t, "text/plain", s), result.values)
+ vecsstring = sprint((t, s) -> show(t, "text/plain", s), result.vectors)
+ λs, ψs, Ts = eigenstates(σx, sparse = true, eigvals = 2)
+ λs1, ψs1, Ts1 = eigenstates(σx, sparse = true, eigvals = 1)
+
+ @test all([ψ.type isa Ket for ψ in ψd])
+ @test typeof(Td) <: AbstractMatrix
+ @test typeof(Ts) <: AbstractMatrix
+ @test typeof(Ts1) <: AbstractMatrix
+ @test all(abs.(eigenenergies(σx, sparse = false)) .≈ abs.(λd))
+ @test all(abs.(eigenenergies(σx, sparse = true, eigvals = 2)) .≈ abs.(λs))
+ @test resstring ==
+ "EigsolveResult: type=$(Operator()) dims=$(result.dims)\nvalues:\n$(valstring)\nvectors:\n$vecsstring"
+
+ N = 30
+ a = kron(destroy(N), qeye(2))
+ a_d = a'
+
+ sm = kron(qeye(N), sigmam())
+ sp = sm'
+ sx = kron(qeye(N), sigmax())
+ sy = kron(qeye(N), sigmay())
+ sz = kron(qeye(N), sigmaz())
+
+ η = 0.2
+ H_d = a_d * a + 0.5 * sz - 1im * η * (a - a_d) * sx + η^2
+ H_c = a_d * a + 0.5 * (sz * cosm(2 * η * (a + a_d)) + sy * sinm(2 * η * (a + a_d)))
+
+ vals_d, vecs_d, mat_d = eigenstates(H_d)
+ vals_c, vecs_c, mat_c = eigenstates(H_c)
+ vals2, vecs2, mat2 = eigenstates(H_d, sparse = true, sigma = -0.9, eigvals = 10, krylovdim = 30, by = real)
+
+ @test real.(vals_d[1:20]) ≈ real.(vals_c[1:20])
+ @test real.(vals_d[1:10]) ≈ real.(vals2[1:10])
+
+ 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_th = 0.01
+
+ H = ωc * a_d * a + ωb * b_d * b + g * (a + a_d) * (b + b_d)
+ c_ops = [√((1 + n_th) * κ) * a, √κ * b, √(n_th * κ) * a_d]
+ L = liouvillian(H, c_ops)
+
+ # eigen solve for general matrices
+ vals, _, vecs = eigsolve(L.data, sigma = 0.01, eigvals = 10, krylovdim = 50)
+ vals2, _, vecs2 = eigenstates(L; sortby = abs)
+ vals3, state3, vecs3 = eigsolve_al(L, 1 \ (40 * κ), eigvals = 10, krylovdim = 50)
+ vals2 = vals2[1:10]
+ vecs2 = vecs2[:, 1:10]
+
+ @test sum(abs2, vals) ≈ sum(abs2, vals2)
+ @test abs2(vals2[1]) ≈ abs2(vals3[1]) atol=1e-7
+ @test vec2mat(vecs[:, 1]) * exp(-1im * angle(vecs[1, 1])) ≈ vec2mat(vecs2[:, 1]) atol=1e-7
+ @test vec2mat(vecs[:, 1]) * exp(-1im * angle(vecs[1, 1])) ≈ vec2mat(vecs3[:, 1]) atol=1e-5
+
+ # eigen solve for QuantumObject
+ result = eigenstates(L, sparse = true, sigma = 0.01, eigvals = 10, krylovdim = 50)
+ vals, vecs = result
+ resstring = sprint((t, s) -> show(t, "text/plain", s), result)
+ valstring = sprint((t, s) -> show(t, "text/plain", s), result.values)
+ vecsstring = sprint((t, s) -> show(t, "text/plain", s), result.vectors)
+ @test resstring ==
+ "EigsolveResult: type=$(SuperOperator()) dims=$(result.dims)\nvalues:\n$(valstring)\nvectors:\n$vecsstring"
+
+ vals2, vecs2 = eigenstates(L, sortby = abs)
+ vals2 = vals2[1:10]
+ vecs2 = vecs2[1:10]
+
+ @test result.type isa SuperOperator
+ @test result.dims == L.dims
+ @test all([v.type isa OperatorKet for v in vecs])
+ @test typeof(result.vectors) <: AbstractMatrix
+ @test sum(abs2, vals) ≈ sum(abs2, vals2)
+ @test abs2(vals2[1]) ≈ abs2(vals3[1]) atol=1e-7
+ @test vec2mat(vecs[1]).data * exp(-1im * angle(vecs[1][1])) ≈ vec2mat(vecs2[1]).data
+ @test 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_th = 0.01
+
+ H = ωc * a_d * a + ωb * b_d * b + g * (a + a_d) * (b + b_d)
+ c_ops = [√((1 + n_th) * κ) * a, √κ * b, √(n_th * κ) * 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 * κ), eigvals = 10)
+ end
+end
diff --git a/test/core-test/enr_state_operator.jl b/test/core-test/enr_state_operator.jl
new file mode 100644
index 000000000..50bed88c8
--- /dev/null
+++ b/test/core-test/enr_state_operator.jl
@@ -0,0 +1,119 @@
+@testitem "Excitation number restricted state space" begin
+ using StaticArraysCore
+
+ @testset "EnrSpace" begin
+ s_enr = EnrSpace((2, 2, 3), 3)
+
+ # check if the idx2state is the same as qutip
+ qutip_idx2state = Dict(
+ 1 => SVector{3}(0, 0, 0),
+ 2 => SVector{3}(0, 0, 1),
+ 3 => SVector{3}(0, 0, 2),
+ 4 => SVector{3}(0, 1, 0),
+ 5 => SVector{3}(0, 1, 1),
+ 6 => SVector{3}(0, 1, 2),
+ 7 => SVector{3}(1, 0, 0),
+ 8 => SVector{3}(1, 0, 1),
+ 9 => SVector{3}(1, 0, 2),
+ 10 => SVector{3}(1, 1, 0),
+ 11 => SVector{3}(1, 1, 1),
+ )
+ @test s_enr.idx2state == qutip_idx2state
+ end
+
+ @testset "kron" begin
+ # normal Space
+ D1 = 4
+ D2 = 5
+ dims_s = (D1, D2)
+ ρ_s = rand_dm(dims_s)
+ I_s = qeye(D1) ⊗ qeye(D2)
+ size_s = prod(dims_s)
+ space_s = (Space(D1), Space(D2))
+
+ # EnrSpace
+ dims_enr = (2, 2, 3)
+ excitations = 3
+ space_enr = EnrSpace(dims_enr, excitations)
+ ρ_enr = enr_thermal_dm(space_enr, rand(3))
+ I_enr = enr_identity(space_enr)
+ size_enr = space_enr.size
+
+ # tensor between normal and ENR space
+ ρ_tot = tensor(ρ_s, ρ_enr)
+ opstring = sprint((t, s) -> show(t, "text/plain", s), ρ_tot)
+ datastring = sprint((t, s) -> show(t, "text/plain", s), ρ_tot.data)
+ ρ_tot_dims = [dims_s..., dims_enr...]
+ ρ_tot_size = size_s * size_enr
+ ρ_tot_isherm = isherm(ρ_tot)
+ @test opstring ==
+ "\nQuantum Object: type=Operator() dims=$ρ_tot_dims size=$((ρ_tot_size, ρ_tot_size)) ishermitian=$ρ_tot_isherm\n$datastring"
+
+ # use GeneralDimensions to do partial trace
+ new_dims1 = GeneralDimensions((Space(1), Space(1), space_enr), (Space(1), Space(1), space_enr))
+ ρ_enr_compound = Qobj(zeros(ComplexF64, size_enr, size_enr), dims = new_dims1)
+ basis_list = [tensor(basis(D1, i), basis(D2, j)) for i in 0:(D1-1) for j in 0:(D2-1)]
+ for b in basis_list
+ ρ_enr_compound += tensor(b', I_enr) * ρ_tot * tensor(b, I_enr)
+ end
+ new_dims2 =
+ GeneralDimensions((space_s..., Space(1), Space(1), Space(1)), (space_s..., Space(1), Space(1), Space(1)))
+ ρ_s_compound = Qobj(zeros(ComplexF64, size_s, size_s), dims = new_dims2)
+ basis_list = [enr_fock(space_enr, space_enr.idx2state[idx]) for idx in 1:space_enr.size]
+ for b in basis_list
+ ρ_s_compound += tensor(I_s, b') * ρ_tot * tensor(I_s, b)
+ end
+ @test ρ_enr.data ≈ ρ_enr_compound.data
+ @test ρ_s.data ≈ ρ_s_compound.data
+ end
+
+ @testset "mesolve, steadystate, and eigenstates" begin
+ ε = 2π
+ ωc = 2π
+ g = 0.1ωc
+ γ = 0.01ωc
+ tlist = range(0, 20, 100)
+ N_cut = 2
+
+ # normal mesolve and steadystate
+ sz = sigmaz() ⊗ qeye(N_cut)
+ sm = sigmam() ⊗ qeye(N_cut)
+ a = qeye(2) ⊗ destroy(N_cut)
+ H_JC = 0.5ε * sz + ωc * a' * a + g * (sm' * a + a' * sm)
+ c_ops_JC = (√γ * a,)
+ ψ0_JC = basis(2, 0) ⊗ fock(N_cut, 0)
+ sol_JC = mesolve(H_JC, ψ0_JC, tlist, c_ops_JC; e_ops = [sz], progress_bar = Val(false))
+ ρ_ss_JC = steadystate(H_JC, c_ops_JC)
+
+ # ENR mesolve and steadystate
+ N_exc = 1
+ dims = (2, N_cut)
+ sm_enr, a_enr = enr_destroy(dims, N_exc)
+ sz_enr = 2 * sm_enr' * sm_enr - 1
+ ψ0_enr = enr_fock(dims, N_exc, [1, 0])
+ H_enr = ε * sm_enr' * sm_enr + ωc * a_enr' * a_enr + g * (sm_enr' * a_enr + a_enr' * sm_enr)
+ c_ops_enr = (√γ * a_enr,)
+ sol_enr = mesolve(H_enr, ψ0_enr, tlist, c_ops_enr; e_ops = [sz_enr], progress_bar = Val(false))
+ ρ_ss_enr = steadystate(H_enr, c_ops_enr)
+
+ # check mesolve result
+ @test all(isapprox.(sol_JC.expect, sol_enr.expect, atol = 1e-4))
+
+ # check steadystate result
+ @test expect(sz, ρ_ss_JC) ≈ expect(sz_enr, ρ_ss_enr) atol=1e-4
+
+ # check eigenstates
+ λ, v = eigenstates(H_enr)
+ @test all([H_enr * v[k] ≈ λ[k] * v[k] for k in eachindex(λ)])
+ end
+
+ @testset "Type Inference" begin
+ N = 3
+ dims = (2, 2, 3)
+ excitations = 3
+ @inferred enr_identity(dims, excitations)
+ @inferred enr_fock(dims, excitations, zeros(Int, N))
+ @inferred enr_destroy(dims, excitations)
+ @inferred enr_thermal_dm(dims, excitations, rand(N))
+ end
+end
diff --git a/test/core-test/entropy_and_metric.jl b/test/core-test/entropy_and_metric.jl
new file mode 100644
index 000000000..79208ca7c
--- /dev/null
+++ b/test/core-test/entropy_and_metric.jl
@@ -0,0 +1,178 @@
+@testitem "entropy" begin
+ base = 2
+ λ = rand()
+ ψ = rand_ket(10)
+ ρ1 = rand_dm(10)
+ ρ2 = rand_dm(10)
+ σ1 = rand_dm(10)
+ σ2 = rand_dm(10)
+
+ dims = (2, 3)
+ ρAB = rand_dm((dims..., dims...))
+ selA = (1, 2)
+ selB = (3, 4)
+ ρA = ptrace(ρAB, selA)
+ ρB = ptrace(ρAB, selB)
+ nA = nB = prod(dims)
+ IA = qeye(nA, dims = dims)
+ IB = qeye(nB, dims = dims)
+
+ # quantum relative entropy
+ @test entropy_relative(ρ1, ψ) == Inf
+ @test entropy_relative(ρ1, rand_dm(10, rank = 9)) == Inf
+ @test entropy_relative(ψ, ψ) + 1 ≈ 1
+ @test entropy_relative(λ * ρ1 + (1 - λ) * ρ2, λ * σ1 + (1 - λ) * σ2) <=
+ λ * entropy_relative(ρ1, σ1) + (1 - λ) * entropy_relative(ρ2, σ2) # joint convexity
+
+ # relations between different entropies
+ @test entropy_relative(ρA, IA / nA) ≈ log(nA) - entropy_vn(ρA)
+ @test entropy_relative(ρB, IB / nB; base = base) ≈ log(base, nB) - entropy_vn(ρB; base = base)
+ @test entropy_relative(ρAB, tensor(ρA, ρB)) ≈ entropy_mutual(ρAB, selA, selB)
+ @test entropy_relative(ρAB, tensor(ρA, ρB)) ≈ entropy_mutual(ρAB, selA, selB)
+ @test entropy_relative(ρAB, tensor(ρA, IB / nB)) ≈ log(nB) - entropy_conditional(ρAB, selA)
+ @test entropy_linear(ρ1) == 1 - purity(ρ1)
+
+ ρ_wrong = Qobj(rand(ComplexF64, 10, 10))
+ @test_throws ErrorException entropy_relative(ρ1, ρ_wrong)
+ @test_throws ErrorException entropy_relative(ρ_wrong, ρ1)
+ @test_throws ArgumentError entropy_mutual(ρAB, 1, 3)
+ @test_throws ArgumentError entropy_mutual(ρAB, 1, (3, 4))
+ @test_throws ArgumentError entropy_mutual(ρAB, (1, 2), 3)
+ @test_throws ArgumentError entropy_mutual(ρAB, (1, 2), (1, 3))
+
+ @testset "Type Stability (entropy)" begin
+ @inferred entropy_vn(ρ1)
+ @inferred entropy_vn(ρ1, base = base)
+ @inferred entropy_relative(ρ1, ψ)
+ @inferred entropy_relative(ρ1, σ1, base = base)
+ @inferred entropy_linear(ρ1)
+ @inferred entropy_mutual(ρAB, selA, selB)
+ @inferred entropy_conditional(ρAB, selA)
+ end
+end
+
+@testitem "entanglement and concurrence" begin
+ # bell state
+ ψb = bell_state(Val(1), Val(0))
+ ρb = ket2dm(ψb)
+ @test entanglement(ψb, 1) / log(2) ≈ 1
+ @test entanglement(ρb, 1) / log(2) ≈ 1
+ @test concurrence(ψb) ≈ 1
+ @test concurrence(ρb) ≈ 1
+
+ # separable pure state
+ ψs = kron(rand_ket(2), rand_ket(2))
+ @test entanglement(ψs, 1) + 1 ≈ 1
+ @test entanglement(ψs, 2) + 1 ≈ 1
+ @test concurrence(ψs) + 1 ≈ 1
+
+ # this only works for "pure" two-qubit states
+ ψr = rand_ket((2, 2)) # might be an entangled two-qubit state
+ val = concurrence(ψr)
+ @test isapprox(val, sqrt(2 * entropy_linear(ptrace(ψr, 1))); atol = 1e-5) # √(2 * (1 - Tr(ρA^2)))
+ @test isapprox(val, sqrt(2 * entropy_linear(ptrace(ψr, 2))); atol = 1e-5) # √(2 * (1 - Tr(ρB^2)))
+
+ @test_throws ArgumentError entanglement(rand_dm((2, 2)), 1)
+ @test_throws ArgumentError concurrence(rand_dm((2, 3)))
+ @test_throws ArgumentError concurrence(rand_dm(4))
+
+ @testset "Type Stability (entanglement)" begin
+ @inferred entanglement(ψb, 1)
+ @inferred entanglement(ρb, 1)
+ end
+
+ @testset "Type Stability (concurrence)" begin
+ @inferred concurrence(ψb)
+ @inferred concurrence(ρb)
+ end
+end
+
+@testitem "trace and Hilbert-Schmidt distance" begin
+ ψz0 = basis(2, 0)
+ ψz1 = basis(2, 1)
+ ρz0 = to_sparse(ket2dm(ψz0))
+ ρz1 = to_sparse(ket2dm(ψz1))
+ ψx0 = sqrt(0.5) * (basis(2, 0) + basis(2, 1))
+ @test tracedist(ψz0, ψx0) ≈ sqrt(0.5)
+ @test tracedist(ρz0, ψz1) ≈ 1.0
+ @test tracedist(ψz1, ρz0) ≈ 1.0
+ @test tracedist(ρz0, ρz1) ≈ 1.0
+
+ ψ = rand_ket(10)
+ ϕ = rand_ket(10)
+ @test isapprox(tracedist(ψ, ϕ)^2, hilbert_dist(ψ, ϕ) / 2; atol = 1e-6)
+
+ @testset "Type Inference (trace distance)" begin
+ @inferred tracedist(ψz0, ψx0)
+ @inferred tracedist(ρz0, ψz1)
+ @inferred tracedist(ψz1, ρz0)
+ @inferred tracedist(ρz0, ρz1)
+ end
+
+ @testset "Type Inference (Hilbert-Schmidt distance)" begin
+ @inferred hilbert_dist(ψz0, ψx0)
+ @inferred hilbert_dist(ρz0, ψz1)
+ @inferred hilbert_dist(ψz1, ρz0)
+ @inferred hilbert_dist(ρz0, ρz1)
+ end
+end
+
+@testitem "fidelity, Bures metric, and Hellinger distance" begin
+ M0 = rand_dm(5)
+ ψ1 = rand_ket(5)
+ ψ2 = rand_ket(5)
+ M1 = ket2dm(ψ1)
+ b00 = bell_state(Val(0), Val(0))
+ b01 = bell_state(Val(0), Val(1))
+ @test isapprox(fidelity(M0, M1), fidelity(ψ1, M0); atol = 1e-6)
+ @test isapprox(fidelity(ψ1, ψ2), fidelity(ket2dm(ψ1), ket2dm(ψ2)); atol = 1e-6)
+ @test isapprox(fidelity(b00, b00), 1; atol = 1e-6)
+ @test isapprox(bures_dist(b00, b00) + 1, 1; atol = 1e-6)
+ @test isapprox(bures_angle(b00, b00) + 1, 1; atol = 1e-6)
+ @test isapprox(hellinger_dist(b00, b00) + 1, 1; atol = 1e-6)
+ @test isapprox(fidelity(b00, b01) + 1, 1; atol = 1e-6)
+ @test isapprox(bures_dist(b00, b01), √2; atol = 1e-6)
+ @test isapprox(bures_angle(b00, b01), π / 2; atol = 1e-6)
+ @test isapprox(hellinger_dist(b00, b01), √2; atol = 1e-6)
+
+ # some relations between Bures and Hellinger dintances
+ # together with some monotonicity under tensor products
+ # [see arXiv:1611.03449 (2017); section 4.2]
+ ρA = rand_dm(5)
+ ρB = rand_dm(6)
+ ρAB = tensor(ρA, ρB)
+ σA = rand_dm(5)
+ σB = rand_dm(6)
+ σAB = tensor(σA, σB)
+ d_Bu_A = bures_dist(ρA, σA)
+ d_Bu_AB = bures_dist(ρAB, σAB)
+ d_He_A = hellinger_dist(ρA, σA)
+ d_He_AB = hellinger_dist(ρAB, σAB)
+ @test isapprox(fidelity(ρAB, σAB), fidelity(ρA, σA) * fidelity(ρB, σB); atol = 1e-6)
+ @test d_He_AB >= d_Bu_AB
+ @test d_Bu_AB >= d_Bu_A
+ @test isapprox(bures_dist(ρAB, tensor(σA, ρB)), d_Bu_A; atol = 1e-6)
+ @test d_He_AB >= d_He_A
+ @test isapprox(hellinger_dist(ρAB, tensor(σA, ρB)), d_He_A; atol = 1e-6)
+
+ @testset "Type Inference (fidelity)" begin
+ @inferred fidelity(M0, M1)
+ @inferred fidelity(ψ1, M0)
+ @inferred fidelity(ψ1, ψ2)
+ end
+
+ @testset "Type Inference (Hellinger distance)" begin
+ @inferred hellinger_dist(M0, M1)
+ @inferred hellinger_dist(ψ1, M0)
+ @inferred hellinger_dist(ψ1, ψ2)
+ end
+
+ @testset "Type Inference (Bures metric)" begin
+ @inferred bures_dist(M0, M1)
+ @inferred bures_dist(ψ1, M0)
+ @inferred bures_dist(ψ1, ψ2)
+ @inferred bures_angle(M0, M1)
+ @inferred bures_angle(ψ1, M0)
+ @inferred bures_angle(ψ1, ψ2)
+ end
+end
diff --git a/test/generalized_master_equation.jl b/test/core-test/generalized_master_equation.jl
similarity index 60%
rename from test/generalized_master_equation.jl
rename to test/core-test/generalized_master_equation.jl
index 080ab17e7..d3686a92a 100644
--- a/test/generalized_master_equation.jl
+++ b/test/core-test/generalized_master_equation.jl
@@ -1,4 +1,6 @@
-@testset "Generalized Master Equation" begin
+@testitem "Generalized Master Equation" begin
+ using LinearAlgebra
+
N_c = 30
N_trunc = 10
tol = 1e-14
@@ -15,12 +17,12 @@
Tlist = [0, 0.0]
E, U, L1 = liouvillian_generalized(H, fields, Tlist, N_trunc = N_trunc, tol = tol)
- Ω = dense_to_sparse((E'.-E)[1:N_trunc, 1:N_trunc], tol)
+ Ω = to_sparse((E' .- E)[1:N_trunc, 1:N_trunc], tol)
- H_d = Qobj(dense_to_sparse((U'*H*U)[1:N_trunc, 1:N_trunc], tol))
- Xp = Qobj(Ω .* dense_to_sparse(triu((U'*(a+a')*U).data[1:N_trunc, 1:N_trunc], 1), tol))
- a2 = Qobj(dense_to_sparse((U'*a*U).data[1:N_trunc, 1:N_trunc], tol))
- sm2 = Qobj(dense_to_sparse((U'*sm*U).data[1:N_trunc, 1:N_trunc], tol))
+ H_d = Qobj(to_sparse((U'*H*U)[1:N_trunc, 1:N_trunc], tol))
+ Xp = Qobj(Ω .* to_sparse(triu((U'*(a+a')*U).data[1:N_trunc, 1:N_trunc], 1), tol))
+ a2 = Qobj(to_sparse((U'*a*U).data[1:N_trunc, 1:N_trunc], tol))
+ sm2 = Qobj(to_sparse((U'*sm*U).data[1:N_trunc, 1:N_trunc], tol))
# Standard liouvillian case
c_ops = [sqrt(0.01) * a2, sqrt(0.01) * sm2]
@@ -33,14 +35,14 @@
Tlist = [0.2, 0.0]
E, U, L1 = liouvillian_generalized(H, fields, Tlist, N_trunc = N_trunc, tol = tol)
- Ω = dense_to_sparse((E'.-E)[1:N_trunc, 1:N_trunc], tol)
+ Ω = to_sparse((E' .- E)[1:N_trunc, 1:N_trunc], tol)
- H_d = Qobj(dense_to_sparse((U'*H*U)[1:N_trunc, 1:N_trunc], tol))
- Xp = Qobj(Ω .* dense_to_sparse(triu((U'*(a+a')*U).data[1:N_trunc, 1:N_trunc], 1), tol))
- a2 = Qobj(dense_to_sparse((U'*a*U).data[1:N_trunc, 1:N_trunc], tol))
- sm2 = Qobj(dense_to_sparse((U'*sm*U).data[1:N_trunc, 1:N_trunc], tol))
+ H_d = Qobj(to_sparse((U'*H*U)[1:N_trunc, 1:N_trunc], tol))
+ Xp = Qobj(Ω .* to_sparse(triu((U'*(a+a')*U).data[1:N_trunc, 1:N_trunc], 1), tol))
+ a2 = Qobj(to_sparse((U'*a*U).data[1:N_trunc, 1:N_trunc], tol))
+ sm2 = Qobj(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
+ @test abs(expect(Xp' * Xp, steadystate(L1)) - n_thermal(1, Tlist[1])) / n_thermal(1, Tlist[1]) < 1e-4
@testset "Type Inference (liouvillian_generalized)" begin
N_c = 30
diff --git a/test/core-test/low_rank_dynamics.jl b/test/core-test/low_rank_dynamics.jl
new file mode 100644
index 000000000..a939a8d27
--- /dev/null
+++ b/test/core-test/low_rank_dynamics.jl
@@ -0,0 +1,85 @@
+@testitem "Low Rank Dynamics" begin
+ using LinearAlgebra
+
+ # Define lattice
+ Nx, Ny = 2, 3
+ latt = Lattice(Nx = Nx, Ny = Ny)
+ ##
+ N_cut = 2 # Number of states of each mode
+ N_modes = latt.N # Number of modes
+ N = N_cut^N_modes # Total number of states
+ M = latt.N + 1 # Number of states in the LR basis
+
+ # Define initial state
+ ϕ = Vector{QuantumObject{Ket,Dimensions{M - 1,NTuple{M - 1,Space}},Vector{ComplexF64}}}(undef, M)
+ ϕ[1] = kron(fill(basis(2, 1), N_modes)...)
+
+ i = 1
+ for j in 1:N_modes
+ global i += 1
+ i <= M && (ϕ[i] = multisite_operator(latt, j => sigmap()) * ϕ[1])
+ end
+ for k in 1:(N_modes-1)
+ for l in (k+1):N_modes
+ global i += 1
+ i <= M && (ϕ[i] = multisite_operator(latt, k => sigmap(), l => sigmap()) * ϕ[1])
+ end
+ end
+ for i in (i+1):M
+ ϕ[i] = QuantumObject(rand(ComplexF64, size(ϕ[1])[1]), dims = ϕ[1].dims)
+ normalize!(ϕ[i])
+ end
+
+ z = hcat(get_data.(ϕ)...)
+ B = Matrix(Diagonal([1 + 0im; zeros(M - 1)]))
+ S = z' * z # Overlap matrix
+ B = B / tr(S * B) # Normalize B
+ ρ = Qobj(z * B * z', dims = ntuple(i -> N_cut, Val(N_modes))) # Full density matrix
+
+ # Define Hamiltonian and collapse operators
+ Jx = 0.9
+ Jy = 1.04
+ Jz = 1.0
+ hx = 0.0
+ hy = 0.0
+ hz = 0.0
+ γ = 1
+
+ Sx = mapreduce(i -> multisite_operator(latt, i => sigmax()), +, 1:latt.N)
+ Sy = mapreduce(i -> multisite_operator(latt, i => sigmay()), +, 1:latt.N)
+ Sz = mapreduce(i -> multisite_operator(latt, i => sigmaz()), +, 1:latt.N)
+ SFxx = mapreduce(
+ x -> multisite_operator(latt, x[1] => sigmax(), x[2] => sigmax()),
+ +,
+ Iterators.product(1:latt.N, 1:latt.N),
+ )
+
+ H, c_ops = DissipativeIsing(Jx, Jy, Jz, hx, hy, hz, γ, latt; boundary_condition = Val(:periodic_bc), order = 1)
+ e_ops = (Sz,)
+
+ tl = range(0, 10, 100)
+
+ # Full solution
+ sol_me = mesolve(H, ρ, tl, c_ops; e_ops = [e_ops...])
+ Strue = entropy_vn(sol_me.states[end], base = 2) / latt.N
+
+ # Low rank solution
+ function f_entropy(p, z, B)
+ C = p.A0
+ σ = p.Bi
+
+ mul!(C, z, sqrt(B))
+ mul!(σ, C', C)
+ return entropy_vn(Qobj(Hermitian(σ), type = Operator()), base = 2)
+ end
+
+ opt = (err_max = 1e-3, p0 = 0.0, atol_inv = 1e-6, adj_condition = "variational", Δt = 0.0, progress = false)
+
+ sol_lr = lr_mesolve(H, z, B, tl, c_ops; e_ops = e_ops, f_ops = (f_entropy,), opt = opt)
+
+ # Test
+ S_lr = real(sol_lr.fexpect[1, end]) / latt.N
+
+ @test real(sol_me.expect[1, :]) ≈ real(sol_lr.expect[1, :]) atol = 1e-1
+ @test S_lr ≈ Strue atol = 1e-1
+end
diff --git a/test/core-test/negativity_and_partial_transpose.jl b/test/core-test/negativity_and_partial_transpose.jl
new file mode 100644
index 000000000..a554d6065
--- /dev/null
+++ b/test/core-test/negativity_and_partial_transpose.jl
@@ -0,0 +1,62 @@
+@testitem "Negativity and Partial Transpose" begin
+ @testset "negativity" begin
+ rho1 = (1 / 40) * Qobj(
+ [
+ 15 1 1 15
+ 1 5 -3 1
+ 1 -3 5 1
+ 15 1 1 15
+ ];
+ dims = (2, 2),
+ )
+ Neg1 = negativity(rho1, 1)
+ @test Neg1 ≈ 0.25
+ @test negativity(rho1, 2) ≈ Neg1
+ @test negativity(rho1, 1; logarithmic = true) ≈ log2(2 * Neg1 + 1)
+ @test_throws ArgumentError negativity(rho1, 3)
+
+ # a maximally entanglment state with subsystem dimension (3, 2):
+ # (|1,0⟩ - i|2,1⟩) / √2
+ rho2_d = ket2dm((tensor(basis(3, 1), basis(2, 0)) - 1im * tensor(basis(3, 2), basis(2, 1))) / √2)
+ rho2_s = to_sparse(rho2_d)
+ @test negativity(rho2_d, 1) ≈ 0.5
+ @test negativity(rho2_d, 2) ≈ 0.5
+ @test negativity(rho2_s, 1) ≈ 0.5
+ @test negativity(rho2_s, 2) ≈ 0.5
+
+ # a separable state with subsystem dimension (3, 2)
+ rho3_d = tensor(rand_dm(3), rand_dm(2))
+ rho3_s = to_sparse(rho3_d)
+ @test abs(negativity(rho3_d, 1)) < 1e-10
+ @test abs(negativity(rho3_d, 2)) < 1e-10
+ @test abs(negativity(rho3_s, 1)) < 1e-10
+ @test abs(negativity(rho3_s, 2)) < 1e-10
+
+ @testset "Type Inference (negativity)" begin
+ @inferred negativity(rho1, 1)
+ @inferred negativity(rho1, 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_sparse = to_sparse(A_dense)
+ PT = (true, false)
+ for s1 in PT
+ for s2 in PT
+ for s3 in PT
+ mask = [s1, s2, s3]
+ @test partial_transpose(A_dense, mask) == partial_transpose(A_sparse, mask)
+ end
+ end
+ end
+ @test_throws ArgumentError partial_transpose(A_dense, [true])
+ @test_throws ArgumentError partial_transpose(Qobj(zeros(ComplexF64, 3, 2)), [true]) # invalid GeneralDimensions
+
+ @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/progress_bar.jl b/test/core-test/progress_bar.jl
similarity index 96%
rename from test/progress_bar.jl
rename to test/core-test/progress_bar.jl
index 274f5d57d..2792a6e0a 100644
--- a/test/progress_bar.jl
+++ b/test/core-test/progress_bar.jl
@@ -1,4 +1,4 @@
-@testset "Progress Bar" begin
+@testitem "Progress Bar" begin
bar_width = 30
strLength = 67 + bar_width # including "\r" in the beginning of the string
prog = ProgressBar(bar_width, enable = true, bar_width = bar_width, interval = 0.2)
diff --git a/test/quantum_objects.jl b/test/core-test/quantum_objects.jl
similarity index 59%
rename from test/quantum_objects.jl
rename to test/core-test/quantum_objects.jl
index e61ff9964..6e51b849e 100644
--- a/test/quantum_objects.jl
+++ b/test/core-test/quantum_objects.jl
@@ -1,28 +1,43 @@
-@testset "Quantum Objects" verbose = true begin
+@testitem "Quantum Objects" begin
+ using LinearAlgebra
+ using SparseArrays
+ using StaticArraysCore
+
# ArgumentError: type is incompatible with vector or matrix
@testset "ArgumentError" begin
a = rand(ComplexF64, 2)
- for t in [Operator, SuperOperator, Bra, OperatorBra]
+ for t in (Operator(), SuperOperator(), Bra(), OperatorBra())
@test_throws ArgumentError Qobj(a, type = t)
end
a = rand(ComplexF64, 2, 2)
- @test_throws ArgumentError Qobj(a, type = Ket)
- @test_throws ArgumentError Qobj(a, type = OperatorKet)
+ @test_throws ArgumentError Qobj(a, type = Ket())
+ @test_throws ArgumentError Qobj(a, type = OperatorKet())
end
# DomainError: incompatible between size of array and type
@testset "DomainError" begin
a = rand(ComplexF64, 3, 2)
- for t in [nothing, Operator, SuperOperator, Bra, OperatorBra]
+ for t in [SuperOperator(), Bra(), OperatorBra()]
@test_throws DomainError Qobj(a, type = t)
end
+
a = rand(ComplexF64, 2, 2, 2)
- for t in [nothing, Ket, Bra, Operator, SuperOperator, OperatorBra, OperatorKet]
+ for t in (nothing, Ket(), Bra(), Operator(), SuperOperator(), OperatorBra(), OperatorKet())
@test_throws DomainError Qobj(a, type = t)
end
+
a = rand(ComplexF64, 1, 2)
- @test_throws DomainError Qobj(a, type = Operator)
- @test_throws DomainError Qobj(a, type = SuperOperator)
+ @test_throws DomainError Qobj(a, type = Operator())
+ @test_throws DomainError Qobj(a, type = SuperOperator())
+
+ @test_throws DomainError Qobj(rand(ComplexF64, 2, 1), type = Operator()) # should be type = Bra
+
+ # check that Ket, Bra, SuperOperator, OperatorKet, and OperatorBra don't support GeneralDimensions
+ @test_throws DomainError Qobj(rand(ComplexF64, 2), type = Ket(), dims = ((2,), (1,)))
+ @test_throws DomainError Qobj(rand(ComplexF64, 1, 2), type = Bra(), dims = ((1,), (2,)))
+ @test_throws DomainError Qobj(rand(ComplexF64, 4, 4), type = SuperOperator(), dims = ((2,), (2,)))
+ @test_throws DomainError Qobj(rand(ComplexF64, 4), type = OperatorKet(), dims = ((2,), (1,)))
+ @test_throws DomainError Qobj(rand(ComplexF64, 1, 4), type = OperatorBra(), dims = ((1,), (2,)))
end
# unsupported type of dims
@@ -33,7 +48,7 @@
@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]`.",
+ "The argument dims should be a Tuple or a StaticVector for better performance. Try to use `dims = (2, 2)` instead of `dims = [2, 2]`. Alternatively, you can do `import QuantumToolbox: SVector` and use `dims = SVector(2, 2)`.",
) Qobj(rand(4, 4), dims = [2, 2])
end
@@ -65,34 +80,62 @@
end
@testset "Operator and SuperOperator" begin
+ N = 10
+ A = Qobj(rand(ComplexF64, N, N))
+ B = Qobj(rand(ComplexF64, N, N))
+ ρ = rand_dm(N) # random density matrix
+ @test mat2vec(A * ρ * B) ≈ spre(A) * spost(B) * mat2vec(ρ) ≈ sprepost(A, B) * mat2vec(ρ) # we must make sure this equality holds !
+
a = sprand(ComplexF64, 100, 100, 0.1)
a2 = Qobj(a)
- a3 = Qobj(a, type = SuperOperator)
-
+ a3 = Qobj(a, type = SuperOperator())
+ a4 = Qobj(sprand(ComplexF64, 100, 10, 0.1)) # GeneralDimensions
+ a5 = QuantumObject(rand(ComplexF64, 2*3*4, 5), dims = ((2, 3, 4), (5,)))
@test isket(a2) == false
@test isbra(a2) == false
@test isoper(a2) == true
@test issuper(a2) == false
@test isoperket(a2) == false
@test isoperbra(a2) == false
+ @test iscached(a2) == true
+ @test isconstant(a2) == true
+ @test isunitary(a2) == false
+ @test a2.dims == [100]
@test isket(a3) == false
@test isbra(a3) == false
@test isoper(a3) == false
@test issuper(a3) == true
@test isoperket(a3) == false
@test isoperbra(a3) == false
+ @test iscached(a3) == true
+ @test isconstant(a3) == true
@test isunitary(a3) == false
+ @test a3.dims == [10]
+ @test isket(a4) == false
+ @test isbra(a4) == false
+ @test isoper(a4) == true
+ @test issuper(a4) == false
+ @test isoperket(a4) == false
+ @test isoperbra(a4) == false
+ @test iscached(a4) == true
+ @test isconstant(a4) == true
+ @test isunitary(a4) == false
+ @test a4.dims == [[100], [10]]
+ @test isoper(a5) == true
+ @test a5.dims == [[2, 3, 4], [5]]
@test_throws DimensionMismatch Qobj(a, dims = 2)
+ @test_throws DimensionMismatch Qobj(a4.data, dims = 2)
+ @test_throws DimensionMismatch Qobj(a4.data, dims = ((100,), (2,)))
end
@testset "OperatorKet and OperatorBra" begin
H = 0.3 * sigmax() + 0.7 * sigmaz()
L = liouvillian(H)
ρ = Qobj(rand(ComplexF64, 2, 2))
- ρ_ket = mat2vec(ρ)
+ ρ_ket = operator_to_vector(ρ)
ρ_bra = ρ_ket'
- @test ρ_bra == Qobj(mat2vec(ρ.data)', type = OperatorBra)
- @test ρ == vec2mat(ρ_ket)
+ @test ρ_bra == Qobj(operator_to_vector(ρ.data)', type = OperatorBra())
+ @test ρ == vector_to_operator(ρ_ket)
@test isket(ρ_ket) == false
@test isbra(ρ_ket) == false
@test isoper(ρ_ket) == false
@@ -116,18 +159,39 @@
@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 "Checks on non-QuantumObjects" begin
+ x = 1
+ @test isket(x) == false
+ @test isbra(x) == false
+ @test isoper(x) == false
+ @test issuper(x) == false
+ @test isoperket(x) == false
+ @test isoperbra(x) == false
+
+ x = rand(ComplexF64, 2)
+ @test isket(x) == false
+ @test isbra(x) == false
+ @test isoper(x) == false
+ @test issuper(x) == false
+ @test isoperket(x) == false
+ @test isoperbra(x) == false
end
@testset "arithmetic" begin
a = sprand(ComplexF64, 100, 100, 0.1)
a2 = Qobj(a)
- a3 = Qobj(a, type = SuperOperator)
- a4 = sparse(a2)
+ a3 = Qobj(a, type = SuperOperator())
+ a4 = to_sparse(a2)
+ a4_copy = copy(a4)
+ a4_copy[1] = rand(ComplexF64)
@test isequal(a4, a2) == true
@test isequal(a4, a3) == false
@test a4 ≈ a2
+ @test a4 != a4_copy
@test real(a2).data == real(a)
@test imag(a2).data == imag(a)
@@ -138,6 +202,14 @@
@test (a2 + 2).data == a2.data + 2 * I
@test a2 * 2 == 2 * a2
+ zero_like = qzero_like(a2)
+ iden_like = qeye_like(a3)
+ zero_array = spzeros(ComplexF64, 100, 100)
+ iden_array = sparse(1:100, 1:100, ones(ComplexF64, 100))
+ @test zero_like == Qobj(zero_array, type = a2.type, dims = a2.dims)
+ @test typeof(zero_like.data) == typeof(zero_array)
+ @test iden_like == Qobj(iden_array, type = a3.type, dims = a3.dims)
+ @test typeof(iden_like.data) == typeof(iden_array)
@test trans(trans(a2)) == a2
@test trans(a2).data == transpose(a2.data)
@test adjoint(a2) ≈ trans(conj(a2))
@@ -197,7 +269,17 @@
a_size = size(a)
a_isherm = isherm(a)
@test opstring ==
- "Quantum Object: type=Operator dims=$a_dims size=$a_size ishermitian=$a_isherm\n$datastring"
+ "\nQuantum Object: type=Operator() dims=$a_dims size=$a_size ishermitian=$a_isherm\n$datastring"
+
+ # GeneralDimensions
+ Gop = tensor(a, ψ)
+ opstring = sprint((t, s) -> show(t, "text/plain", s), Gop)
+ datastring = sprint((t, s) -> show(t, "text/plain", s), Gop.data)
+ Gop_dims = [[N, N], [N, 1]]
+ Gop_size = size(Gop)
+ Gop_isherm = isherm(Gop)
+ @test opstring ==
+ "\nQuantum Object: type=Operator() dims=$Gop_dims size=$Gop_size ishermitian=$Gop_isherm\n$datastring"
a = spre(a)
opstring = sprint((t, s) -> show(t, "text/plain", s), a)
@@ -205,42 +287,42 @@
a_dims = a.dims
a_size = size(a)
a_isherm = isherm(a)
- @test opstring == "Quantum Object: type=SuperOperator dims=$a_dims size=$a_size\n$datastring"
+ @test opstring == "\nQuantum Object: type=SuperOperator() dims=$a_dims size=$a_size\n$datastring"
opstring = sprint((t, s) -> show(t, "text/plain", s), ψ)
datastring = sprint((t, s) -> show(t, "text/plain", s), ψ.data)
ψ_dims = ψ.dims
ψ_size = size(ψ)
- @test opstring == "Quantum Object: type=Ket dims=$ψ_dims size=$ψ_size\n$datastring"
+ @test opstring == "\nQuantum Object: type=Ket() dims=$ψ_dims size=$ψ_size\n$datastring"
ψ = ψ'
opstring = sprint((t, s) -> show(t, "text/plain", s), ψ)
datastring = sprint((t, s) -> show(t, "text/plain", s), ψ.data)
ψ_dims = ψ.dims
ψ_size = size(ψ)
- @test opstring == "Quantum Object: type=Bra dims=$ψ_dims size=$ψ_size\n$datastring"
+ @test opstring == "\nQuantum Object: type=Bra() dims=$ψ_dims size=$ψ_size\n$datastring"
- ψ2 = Qobj(rand(ComplexF64, 4), type = OperatorKet)
+ ψ2 = Qobj(rand(ComplexF64, 4), type = OperatorKet())
opstring = sprint((t, s) -> show(t, "text/plain", s), ψ2)
datastring = sprint((t, s) -> show(t, "text/plain", s), ψ2.data)
ψ2_dims = ψ2.dims
ψ2_size = size(ψ2)
- @test opstring == "Quantum Object: type=OperatorKet dims=$ψ2_dims size=$ψ2_size\n$datastring"
+ @test opstring == "\nQuantum Object: type=OperatorKet() dims=$ψ2_dims size=$ψ2_size\n$datastring"
ψ2 = ψ2'
opstring = sprint((t, s) -> show(t, "text/plain", s), ψ2)
datastring = sprint((t, s) -> show(t, "text/plain", s), ψ2.data)
ψ2_dims = ψ2.dims
ψ2_size = size(ψ2)
- @test opstring == "Quantum Object: type=OperatorBra dims=$ψ2_dims size=$ψ2_size\n$datastring"
+ @test opstring == "\nQuantum Object: type=OperatorBra() dims=$ψ2_dims size=$ψ2_size\n$datastring"
end
@testset "matrix element" begin
H = Qobj([1 2; 3 4])
L = liouvillian(H)
- s0 = Qobj(basis(4, 0).data; type = OperatorKet)
- s1 = Qobj(basis(4, 1).data; type = OperatorKet)
- s_wrong = Qobj(basis(9, 0).data; type = OperatorKet)
+ s0 = Qobj(basis(4, 0).data; type = OperatorKet())
+ s1 = Qobj(basis(4, 1).data; type = OperatorKet())
+ s_wrong = Qobj(basis(9, 0).data; type = OperatorKet())
@test matrix_element(basis(2, 0), H, basis(2, 1)) == H[1, 2]
@test matrix_element(s0, L, s1) == L[1, 2]
@test_throws DimensionMismatch matrix_element(basis(3, 0), H, basis(2, 1))
@@ -251,9 +333,9 @@
@testset "element type conversion" begin
vd = Qobj(Int64[0, 0])
- vs = Qobj(dense_to_sparse(vd))
+ vs = Qobj(to_sparse(vd))
Md = Qobj(Int64[0 0; 0 0])
- Ms = Qobj(dense_to_sparse(Md))
+ Ms = Qobj(to_sparse(Md))
@test typeof(Vector(vd).data) == Vector{Int64}
@test typeof(Vector(vs).data) == Vector{Int64}
@test typeof(Vector{ComplexF64}(vd).data) == Vector{ComplexF64}
@@ -268,30 +350,40 @@
@test typeof(SparseMatrixCSC(Md).data) == SparseMatrixCSC{Int64,Int64}
@test typeof(SparseMatrixCSC(Ms).data) == SparseMatrixCSC{Int64,Int64}
@test typeof(SparseMatrixCSC{ComplexF64}(Ms).data) == SparseMatrixCSC{ComplexF64,Int64}
+
+ @testset "Deprecated Errors" begin
+ @test_throws ErrorException sparse_to_dense(vs)
+ @test_throws ErrorException dense_to_sparse(vd)
+ end
end
@testset "Type Inference (QuantumObject)" begin
- for T in [ComplexF32, ComplexF64]
+ 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)
+ for type in (Ket(), OperatorKet())
@inferred Qobj(a, type = type)
end
- UnionType =
- Union{QuantumObject{Matrix{T},BraQuantumObject,1},QuantumObject{Matrix{T},OperatorQuantumObject,1}}
+ UnionType = Union{
+ QuantumObject{Bra,Dimensions{1,Tuple{Space}},Matrix{T}},
+ QuantumObject{Operator,Dimensions{1,Tuple{Space}},Matrix{T}},
+ }
a = rand(T, 1, N)
@inferred UnionType Qobj(a)
- for type in [Bra, OperatorBra]
+ for type in (Bra(), OperatorBra())
@inferred Qobj(a, type = type)
end
+ UnionType2 = Union{
+ QuantumObject{Operator,GeneralDimensions{1,1,Tuple{Space},Tuple{Space}},Matrix{T}},
+ QuantumObject{Operator,Dimensions{1,Tuple{Space}},Matrix{T}},
+ }
a = rand(T, N, N)
@inferred UnionType Qobj(a)
- for type in [Operator, SuperOperator]
- @inferred Qobj(a, type = type)
- end
+ @inferred UnionType2 Qobj(a, type = Operator())
+ @inferred Qobj(a, type = SuperOperator())
end
@testset "Math Operation" begin
@@ -309,15 +401,28 @@
@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
ψ = fock(N, 3)
- @test proj(ψ) ≈ proj(ψ') ≈ sparse(ket2dm(ψ)) ≈ projection(N, 3, 3)
+ @test proj(ψ) ≈ proj(ψ') ≈ to_sparse(ket2dm(ψ)) ≈ projection(N, 3, 3)
@test isket(ψ') == false
@test isbra(ψ') == true
@test shape(ψ) == (N,)
@@ -384,6 +489,23 @@
@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)
+
+ # when input is a vector of observables
+ ρlist = Hermitian.(ket2dm.(ψlist)) # an alternative way to calculate expectation values for a list of density matrices
+ Olist1 = [a' * a, a' + a, a]
+ Olist2 = [Hermitian(a' * a), Hermitian(a' + a)]
+ exp_val_1 = expect(Olist1, ψlist)
+ exp_val_2 = expect(Olist2, ψlist)
+ @test size(exp_val_1) == (3, 4)
+ @test size(exp_val_2) == (2, 4)
+ @test all(exp_val_1[1, :] .≈ exp_val_2[1, :] .≈ expect(ρlist, a' * a))
+ @test all(exp_val_1[2, :] .≈ exp_val_2[2, :] .≈ expect(ρlist, a' + a))
+ @test all(exp_val_1[3, :] .≈ expect(a, ρlist))
+
@testset "Type Inference (expect)" begin
@inferred expect(a, ψ)
@inferred expect(a, ψ')
@@ -391,6 +513,14 @@
@inferred variance(a, ψ')
@inferred expect(a, ρ)
@inferred variance(a, ρ)
+ @inferred expect(a, ψlist)
+ @inferred variance(a, ψlist)
+ @inferred expect(ρlist, a)
+ @inferred expect(Olist1, ψ)
+ @inferred expect(Olist1, ψ')
+ @inferred expect(Olist1, ρ)
+ @inferred expect(Olist1, ψlist)
+ @inferred expect(Olist2, ψlist)
end
end
@@ -446,54 +576,20 @@
end
end
- @testset "trace distance" begin
- ψz0 = basis(2, 0)
- ψz1 = basis(2, 1)
- ρz0 = dense_to_sparse(ket2dm(ψz0))
- ρz1 = dense_to_sparse(ket2dm(ψz1))
- ψx0 = sqrt(0.5) * (basis(2, 0) + basis(2, 1))
- @test tracedist(ψz0, ψx0) ≈ sqrt(0.5)
- @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
- M = sprand(ComplexF64, 5, 5, 0.5)
- M0 = Qobj(M * M')
- ψ1 = Qobj(rand(ComplexF64, 5))
- ψ2 = Qobj(rand(ComplexF64, 5))
- M1 = ψ1 * ψ1'
- @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
+ @testset "sqrt, log, exp, sinm, cosm" begin
M0 = rand(ComplexF64, 4, 4)
Md = Qobj(M0 * M0')
- Ms = dense_to_sparse(Md)
+ Ms = to_sparse(Md)
e_p = expm(1im * Md)
e_m = expm(-1im * Md)
+ @test sqrtm(Md) ≈ sqrtm(Ms)
@test logm(expm(Ms)) ≈ expm(logm(Md))
@test cosm(Ms) ≈ (e_p + e_m) / 2
@test sinm(Ms) ≈ (e_p - e_m) / 2im
@testset "Type Inference" begin
+ @inferred sqrtm(Md)
+ @inferred sqrtm(Ms)
@inferred expm(Md)
@inferred expm(Ms)
@inferred logm(Md)
@@ -510,28 +606,28 @@
tol = 0.5
## Vector{Float64} with in-place tidyup
ψ1 = Qobj(rand(Float64, N))
- ψ2 = dense_to_sparse(ψ1)
+ ψ2 = to_sparse(ψ1)
@test tidyup!(ψ2, tol) == ψ2 != ψ1
- @test dense_to_sparse(tidyup!(ψ1, tol)) == ψ2
+ @test to_sparse(tidyup!(ψ1, tol)) == ψ2
## Vector{Float64} with normal tidyup
ψ1 = Qobj(rand(Float64, N))
- ψ2 = dense_to_sparse(ψ1)
+ ψ2 = to_sparse(ψ1)
@test tidyup(ψ2, tol) != ψ2
- @test dense_to_sparse(tidyup(ψ1, tol)) == tidyup(ψ2, tol)
+ @test to_sparse(tidyup(ψ1, tol)) == tidyup(ψ2, tol)
## Matrix{ComplexF64} with in-place tidyup
tol = 0.1
ρ1 = rand_dm(N)
- ρ2 = dense_to_sparse(ρ1)
+ ρ2 = to_sparse(ρ1)
@test tidyup!(ρ2, tol) == ρ2 != ρ1
- @test dense_to_sparse(tidyup!(ρ1, tol)) == ρ2
+ @test to_sparse(tidyup!(ρ1, tol)) == ρ2
## Matrix{ComplexF64} with normal tidyup
ρ1 = rand_dm(N)
- ρ2 = dense_to_sparse(ρ1)
+ ρ2 = to_sparse(ρ1)
@test tidyup(ρ2, tol) != ρ2
- @test dense_to_sparse(tidyup(ρ1, tol)) == tidyup(ρ2, tol)
+ @test to_sparse(tidyup(ρ1, tol)) == tidyup(ρ2, tol)
@testset "Type Inference (tidyup)" begin
@inferred tidyup(ψ1, tol)
@@ -573,8 +669,79 @@
ρ = 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
+
+ # use GeneralDimensions to do partial trace
+ ρ1_compound = Qobj(zeros(ComplexF64, 2, 2), dims = ((2, 1), (2, 1)))
+ II = qeye(2)
+ basis_list = [basis(2, i) for i in 0:1]
+ for b in basis_list
+ ρ1_compound += tensor(II, b') * ρ * tensor(II, b)
+ end
+ ρ2_compound = Qobj(zeros(ComplexF64, 2, 2), dims = ((1, 2), (1, 2)))
+ for b in basis_list
+ ρ2_compound += tensor(b', II) * ρ * tensor(b, II)
+ end
+ @test ρ1.data ≈ ρ1_ptr.data ≈ ρ1_compound.data
+ @test ρ2.data ≈ ρ2_ptr.data ≈ ρ2_compound.data
+ @test ρ1.dims != ρ1_compound.dims
+ @test ρ2.dims != ρ2_compound.dims
+ ρ1_compound = ptrace(ρ1_compound, 1)
+ ρ2_compound = ptrace(ρ2_compound, 2)
+ @test ρ1.dims == ρ1_compound.dims
+ @test ρ2.dims == ρ2_compound.dims
+
+ ψ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)` instead of `sel = [1, 2]`. Alternatively, you can do `import QuantumToolbox: SVector` and use `sel = SVector(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)` instead of `sel = [1, 2]`. Alternatively, you can do `import QuantumToolbox: SVector` and use `sel = SVector(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))
+ @test_throws ArgumentError ptrace(Qobj(zeros(ComplexF64, 3, 2)), 1) # invalid GeneralDimensions
@testset "Type Inference (ptrace)" begin
@inferred ptrace(ρ, 1)
@@ -587,6 +754,7 @@
end
@testset "permute" begin
+ # standard Dimensions
ket_a = Qobj(rand(ComplexF64, 2))
ket_b = Qobj(rand(ComplexF64, 3))
ket_c = Qobj(rand(ComplexF64, 4))
@@ -621,10 +789,22 @@
@test_throws ArgumentError permute(op_bdca, wrong_order1)
@test_throws ArgumentError permute(op_bdca, wrong_order2)
+ # GeneralDimensions
+ Gop_d = Qobj(rand(ComplexF64, 5, 6))
+ compound_bdca = permute(tensor(ket_a, op_b, bra_c, Gop_d), (2, 4, 3, 1))
+ compound_dacb = permute(tensor(ket_a, op_b, bra_c, Gop_d), (4, 1, 3, 2))
+ @test compound_bdca ≈ tensor(op_b, Gop_d, bra_c, ket_a)
+ @test compound_dacb ≈ tensor(Gop_d, ket_a, bra_c, op_b)
+ @test compound_bdca.dims == [[3, 5, 1, 2], [3, 6, 4, 1]]
+ @test compound_dacb.dims == [[5, 2, 1, 3], [6, 1, 4, 3]]
+ @test isoper(compound_bdca)
+ @test isoper(compound_dacb)
+
@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))
+ @inferred permute(compound_bdca, (2, 4, 3, 1))
end
end
end
diff --git a/test/core-test/quantum_objects_evo.jl b/test/core-test/quantum_objects_evo.jl
new file mode 100644
index 000000000..cb86a74fe
--- /dev/null
+++ b/test/core-test/quantum_objects_evo.jl
@@ -0,0 +1,251 @@
+@testitem "Quantum Objects Evolution" begin
+ using LinearAlgebra
+ using SparseArrays
+ using StaticArraysCore
+ using SciMLOperators
+
+ # DomainError: incompatible between size of array and type
+ @testset "Thrown Errors" begin
+ a = MatrixOperator(rand(ComplexF64, 3, 2))
+ @test_throws DomainError QobjEvo(a, type = SuperOperator())
+
+ a = MatrixOperator(rand(ComplexF64, 4, 4))
+ @test_throws DomainError QobjEvo(a, type = SuperOperator(), dims = ((2,), (2,)))
+
+ a = MatrixOperator(rand(ComplexF64, 3, 2))
+ for t in (Ket(), Bra(), OperatorKet(), OperatorBra())
+ @test_throws ArgumentError QobjEvo(a, type = t)
+ end
+
+ a = QobjEvo(destroy(20))
+ @test_throws ArgumentError QobjEvo(a, type = SuperOperator())
+
+ a = MatrixOperator(rand(ComplexF64, 5, 5))
+ @test_throws DimensionMismatch QobjEvo(a, type = SuperOperator())
+
+ ψ = fock(10, 3)
+ @test_throws MethodError QobjEvo(ψ)
+ end
+
+ # unsupported type of dims
+ @testset "unsupported dims" begin
+ a = MatrixOperator(rand(2, 2))
+ @test_throws ArgumentError QobjEvo(a, dims = 2.0)
+ @test_throws ArgumentError QobjEvo(a, dims = 2.0 + 0.0im)
+ @test_throws DomainError QobjEvo(a, dims = 0)
+ @test_throws DomainError QobjEvo(a, 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)` instead of `dims = [2, 2]`. Alternatively, you can do `import QuantumToolbox: SVector` and use `dims = SVector(2, 2)`.",
+ ) QobjEvo(MatrixOperator(rand(4, 4)), dims = [2, 2])
+ end
+
+ @testset "Operator and SuperOperator" begin
+ a = MatrixOperator(sprand(ComplexF64, 100, 100, 0.1))
+ a2 = QobjEvo(a)
+ a3 = QobjEvo(a, type = SuperOperator())
+
+ @test isket(a2) == false
+ @test isbra(a2) == false
+ @test isoper(a2) == true
+ @test issuper(a2) == false
+ @test isoperket(a2) == false
+ @test isoperbra(a2) == false
+ @test isket(a3) == false
+ @test isbra(a3) == false
+ @test isoper(a3) == false
+ @test issuper(a3) == true
+ @test isoperket(a3) == false
+ @test isoperbra(a3) == false
+ @test_throws DimensionMismatch QobjEvo(a, dims = 2)
+ end
+
+ @testset "Promote Operators Type" begin
+ a = destroy(20)
+ A = QobjEvo(a)
+ @test QuantumToolbox.promote_op_type(a, A) == QuantumObjectEvolution
+ @test QuantumToolbox.promote_op_type(A, a) == QuantumObjectEvolution
+ @test QuantumToolbox.promote_op_type(A, A) == QuantumObjectEvolution
+ @test QuantumToolbox.promote_op_type(a, a) == QuantumObject
+ end
+
+ @testset "arithmetic" begin
+ a = MatrixOperator(sprand(ComplexF64, 100, 100, 0.1))
+ a2 = QobjEvo(a)
+ a3 = QobjEvo(a, type = SuperOperator())
+
+ @test +a2 == a2
+ @test -(-a2) == a2
+ @test a2 + 2 == 2 + a2
+ @test (a2 + 2).data == a2.data + 2 * I
+ @test a2 * 2 == 2 * a2
+
+ zero_like = qzero_like(a2)
+ iden_like = qeye_like(a3)
+ zero_array = NullOperator(100)
+ iden_array = IdentityOperator(100)
+ @test zero_like == QobjEvo(zero_array, type = a2.type, dims = a2.dims)
+ @test typeof(zero_like.data) == typeof(zero_array)
+ @test iden_like == QobjEvo(iden_array, type = a3.type, dims = a3.dims)
+ @test typeof(iden_like.data) == typeof(iden_array)
+ @test trans(trans(a2)) == a2
+ @test trans(a2).data == transpose(a2.data)
+ # @test adjoint(a2) ≈ trans(conj(a2)) # Currently doesn't work
+ @test adjoint(adjoint(a2)) == a2
+ @test adjoint(a2).data == adjoint(a2.data)
+
+ N = 10
+ # We use MatrixOperator instead of directly using a Qobj to increase coverage
+ a = QobjEvo(MatrixOperator(sprand(ComplexF64, N, N, 5 / N)), Operator(), N)
+ a_d = a'
+ X = a + a_d
+ # Y = 1im * (a - a_d) # Currently doesn't work. Fix in SciMLOperators.jl
+ Z = a + trans(a)
+ @test isherm(X) == true
+ # @test isherm(Y) == true
+ # @test issymmetric(Y) == false
+ @test issymmetric(Z) == true
+ end
+
+ @testset "REPL show" begin
+ N = 10
+ a = destroy(N)
+ coef(p, t) = exp(-1im * t)
+ H = QobjEvo((a' * a, (a, coef)))
+
+ opstring = sprint((t, s) -> show(t, "text/plain", s), H)
+ datastring = sprint((t, s) -> show(t, "text/plain", s), H.data)
+ H_dims = H.dims
+ H_size = size(H)
+ H_isherm = isherm(H)
+ H_isconst = isconstant(H)
+ @test opstring ==
+ "\nQuantum Object Evo.: type=Operator() dims=$H_dims size=$H_size ishermitian=$H_isherm isconstant=$H_isconst\n$datastring"
+
+ L = QobjEvo(spre(a))
+ opstring = sprint((t, s) -> show(t, "text/plain", s), L)
+ datastring = sprint((t, s) -> show(t, "text/plain", s), L.data)
+ L_dims = L.dims
+ L_size = size(L)
+ L_isherm = isherm(L)
+ L_isconst = isconstant(L)
+ @test opstring ==
+ "\nQuantum Object Evo.: type=SuperOperator() dims=$L_dims size=$L_size ishermitian=$L_isherm isconstant=$L_isconst\n$datastring"
+ end
+
+ @testset "Type Inference (QobjEvo)" begin
+ N = 4
+ for T in [ComplexF32, ComplexF64]
+ a = MatrixOperator(rand(T, N, N))
+ UnionType = Union{
+ QuantumObjectEvolution{Operator,GeneralDimensions{1,Tuple{Space},Tuple{Space}},typeof(a)},
+ QuantumObjectEvolution{Operator,Dimensions{1,Tuple{Space}},typeof(a)},
+ }
+ @inferred UnionType QobjEvo(a)
+ @inferred UnionType QobjEvo(a, type = Operator())
+ @inferred QobjEvo(a, type = SuperOperator())
+ end
+
+ a = destroy(N)
+ coef1(p, t) = exp(-t)
+ coef2(p::Vector, t) = sin(p[1] * t)
+ coef3(p::NamedTuple, t) = cos(p.ω * t)
+ @inferred QobjEvo(a, coef1)
+ @inferred QobjEvo((a', coef2))
+ @inferred QobjEvo((a' * a, (a, coef1), (a', coef2), (a + a', coef3)))
+
+ @testset "Math Operation" begin
+ a = QobjEvo(destroy(20))
+ σx = QobjEvo(sigmax())
+ @inferred a + a
+ @inferred a + a'
+ # @inferred a + 2 # TODO fix in SciMLOperators.jl
+ @inferred 2 * a
+ @inferred a / 2
+ @inferred a * a
+ @inferred a * a'
+
+ @inferred kron(a)
+ @test_logs (:warn,) @inferred kron(a, σx)
+ @test_logs (:warn,) @inferred kron(a, eye(2))
+ @test_logs (:warn,) (:warn,) @inferred kron(a, eye(2), eye(2))
+ end
+ end
+
+ @testset "tensor" begin
+ σx = QobjEvo(sigmax())
+ X3 = @test_logs (:warn,) (:warn,) tensor(σx, σx, σx)
+ X_warn = @test_logs (:warn,) (:warn,) (:warn,) tensor(fill(σx, 3))
+ @test X_warn(0) == X3(0) == tensor(sigmax(), sigmax(), sigmax())
+ end
+
+ @testset "Time Dependent Operators and SuperOperators" begin
+ N = 10
+ a = destroy(N)
+ coef1(p, t) = exp(-1im * p.ω1 * t)
+ coef2(p, t) = sin(p.ω2 * t)
+ coef3(p, t) = sin(p.ω3 * t)
+ t = rand()
+ p = (ω1 = rand(), ω2 = rand(), ω3 = rand())
+
+ # Operator
+ H_td = QobjEvo(((a, coef1), a' * a, (a', coef2)))
+ H_ti = coef1(p, t) * a + a' * a + coef2(p, t) * a'
+ ψ = rand_ket(N)
+ @test H_td(p, t) ≈ H_ti
+ @test iscached(H_td) == true
+ H_td = cache_operator(H_td, ψ)
+ @test iscached(H_td) == true
+ @test H_td(ψ, p, t) ≈ H_ti * ψ
+ @test isconstant(a) == true
+ @test isconstant(H_td) == false
+ @test isconstant(QobjEvo(a)) == true
+ @test isoper(H_td) == true
+ @test QobjEvo(a, coef1) == QobjEvo((a, coef1))
+
+ # SuperOperator
+ X = a * a'
+ c_op1 = QobjEvo(a', coef1)
+ c_op2 = QobjEvo(((a, coef2), (X, coef3)))
+ c_ops = [c_op1, c_op2]
+ D1_ti = abs2(coef1(p, t)) * lindblad_dissipator(a')
+ D2_ti =
+ abs2(coef2(p, t)) * lindblad_dissipator(a) + # normal dissipator for first element in c_op2
+ abs2(coef3(p, t)) * lindblad_dissipator(X) + # normal dissipator for second element in c_op2
+ coef2(p, t) * conj(coef3(p, t)) * (spre(a) * spost(X') - 0.5 * spre(X' * a) - 0.5 * spost(X' * a)) + # cross terms
+ conj(coef2(p, t)) * coef3(p, t) * (spre(X) * spost(a') - 0.5 * spre(a' * X) - 0.5 * spost(a' * X)) # cross terms
+ L_ti = liouvillian(H_ti) + D1_ti + D2_ti
+ L_td = @test_logs (:warn,) (:warn,) liouvillian(H_td, c_ops) # warnings from lazy tensor in `lindblad_dissipator(c_op2)`
+ ρvec = mat2vec(rand_dm(N))
+ @test L_td(p, t) ≈ L_ti
+ @test iscached(L_td) == false
+ L_td = cache_operator(L_td, ρvec)
+ @test iscached(L_td) == true
+ @test L_td(ρvec, p, t) ≈ L_ti * ρvec
+ @test isconstant(L_td) == false
+ @test issuper(L_td) == true
+
+ coef_wrong1(t) = nothing
+ coef_wrong2(p, t::ComplexF64) = nothing
+ @test_logs (:warn,) (:warn,) liouvillian(H_td * H_td) # warnings from lazy tensor
+ @test_throws ArgumentError QobjEvo(a, coef_wrong1)
+ @test_throws ArgumentError QobjEvo(a, coef_wrong2)
+ @test_throws MethodError QobjEvo([[a, coef1], a' * a, [a', coef2]])
+ @test_throws ArgumentError H_td(ρvec, p, t)
+ @test_throws ArgumentError cache_operator(H_td, ρvec)
+ @test_throws ArgumentError L_td(ψ, p, t)
+ @test_throws ArgumentError cache_operator(L_td, ψ)
+
+ @testset "Type Inference" begin
+ # we use destroy and create here because they somehow causes type instability before
+ H_td2 = H_td + QobjEvo(destroy(N) + create(N), coef3)
+ c_ops1 = (destroy(N), create(N))
+ c_ops2 = (destroy(N), QobjEvo(create(N), coef1))
+
+ @inferred liouvillian(H_td, c_ops1)
+ @inferred liouvillian(H_td, c_ops2)
+ @inferred liouvillian(H_td2, c_ops1)
+ @inferred liouvillian(H_td2, c_ops2)
+ end
+ end
+end
diff --git a/test/states_and_operators.jl b/test/core-test/states_and_operators.jl
similarity index 93%
rename from test/states_and_operators.jl
rename to test/core-test/states_and_operators.jl
index 08aa0c953..6d0aef108 100644
--- a/test/states_and_operators.jl
+++ b/test/core-test/states_and_operators.jl
@@ -1,4 +1,8 @@
-@testset "States and Operators" verbose = true begin
+@testitem "States and Operators" begin
+ import QuantumToolbox: position, momentum
+ using LinearAlgebra
+ using SparseArrays
+
@testset "zero state" begin
v1 = zero_ket(4)
v2 = zero_ket((2, 2))
@@ -283,13 +287,13 @@
sites = 4
SIZE = 2^sites
dims = ntuple(i -> 2, Val(sites))
- Q_iden = Qobj(sparse((1.0 + 0.0im) * LinearAlgebra.I, SIZE, SIZE); dims = dims)
+ Q_iden = Qobj(sparse((1.0 + 0.0im) * 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
@@ -301,16 +305,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_su1 = qeye(4, type = SuperOperator)
- I_su2 = qeye(4, type = SuperOperator, dims = 2)
+ I_su1 = qeye(4, type = SuperOperator())
+ I_su2 = qeye(4, type = SuperOperator(), dims = 2)
@test isunitary(I_op1) == true
@test isunitary(I_op2) == true
@test isunitary(I_su1) == false
@@ -323,14 +338,14 @@
@test (I_op2 == I_su2) == false
@test (I_su1 == I_su2) == true
@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(2, type = SuperOperator())
+ @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)
+ Xd = to_dense(Xs)
A_wrong1 = Qobj(rand(4, 4), dims = 4)
A_wrong2 = Qobj(rand(4, 4), dims = (2, 2))
A_wrong3 = Qobj(rand(3, 3))
diff --git a/test/steady_state.jl b/test/core-test/steady_state.jl
similarity index 62%
rename from test/steady_state.jl
rename to test/core-test/steady_state.jl
index 924cc0907..6151af349 100644
--- a/test/steady_state.jl
+++ b/test/core-test/steady_state.jl
@@ -1,8 +1,9 @@
-@testset "Steady State" begin
+@testitem "Steady State" begin
N = 10
a = destroy(N)
a_d = a'
H = a_d * a + 0.1 * (a + a_d)
+ Ht = QobjEvo(H, (p, t) -> 1) # for test throw
c_ops = [sqrt(0.1) * a]
e_ops = [a_d * a]
psi0 = fock(N, 3)
@@ -11,37 +12,32 @@
rho_me = sol_me.states[end]
solver = SteadyStateODESolver()
- ρ_ss = steadystate(H, psi0, t_l[end], c_ops, solver = solver)
- @test tracedist(rho_me, ρ_ss) < 1e-4
-
- solver = SteadyStateDirectSolver()
ρ_ss = steadystate(H, c_ops, solver = solver)
@test tracedist(rho_me, ρ_ss) < 1e-4
- solver = SteadyStateLinearSolver()
+ solver = SteadyStateDirectSolver()
ρ_ss = steadystate(H, c_ops, solver = solver)
@test tracedist(rho_me, ρ_ss) < 1e-4
+ @test_throws ArgumentError steadystate(Ht, c_ops, solver = solver)
solver = SteadyStateLinearSolver()
ρ_ss = steadystate(H, c_ops, solver = solver)
@test tracedist(rho_me, ρ_ss) < 1e-4
+ @test_throws ArgumentError steadystate(Ht, c_ops, solver = solver)
solver = SteadyStateEigenSolver()
ρ_ss = steadystate(H, c_ops, solver = solver)
@test tracedist(rho_me, ρ_ss) < 1e-4
+ @test_throws ArgumentError steadystate(Ht, c_ops, solver = solver)
@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()
+ solver = SteadyStateODESolver(tmax = t_l[end])
@inferred steadystate(H, c_ops, solver = solver)
@inferred steadystate(L, solver = solver)
- solver = SteadyStateLinearSolver()
+ solver = SteadyStateDirectSolver()
@inferred steadystate(H, c_ops, solver = solver)
@inferred steadystate(L, solver = solver)
@@ -63,18 +59,28 @@
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, progress_bar = Val(false))
- ρ_ss1 = steadystate_floquet(H, -1im * 0.5 * H_t, 1im * 0.5 * H_t, 1, c_ops, solver = SSFloquetLinearSystem())[1]
+
+ coeff(p, t) = sin(t)
+ H_td = (H, (H_t, coeff))
+
+ sol_me = mesolve(H_td, psi0, t_l, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ ρ_ss1 = steadystate_fourier(H, -1im * 0.5 * H_t, 1im * 0.5 * H_t, 1, c_ops, solver = SteadyStateLinearSolver())[1]
ρ_ss2 =
- steadystate_floquet(H, -1im * 0.5 * H_t, 1im * 0.5 * H_t, 1, c_ops, solver = SSFloquetEffectiveLiouvillian())
+ steadystate_fourier(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
+ @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(
+ @testset "Type Inference (steadystate_fourier)" begin
+ @inferred steadystate_fourier(
+ H,
+ -1im * 0.5 * H_t,
+ 1im * 0.5 * H_t,
+ 1,
+ c_ops,
+ solver = SteadyStateLinearSolver(),
+ )
+ @inferred steadystate_fourier(
H,
-1im * 0.5 * H_t,
1im * 0.5 * H_t,
diff --git a/test/core-test/time_evolution.jl b/test/core-test/time_evolution.jl
new file mode 100644
index 000000000..d10a7c6d6
--- /dev/null
+++ b/test/core-test/time_evolution.jl
@@ -0,0 +1,1056 @@
+@testmodule TESetup begin
+ using QuantumToolbox
+ using Random
+
+ # Global definition of the system
+ N = 10
+ a = kron(destroy(N), qeye(2))
+ σm = kron(qeye(N), sigmam())
+ σz = qeye(N) ⊗ sigmaz()
+
+ g = 0.01
+ ωc = 1
+ ωq = 0.99
+ γ = 0.1
+ nth = 0.001
+
+ # Jaynes-Cummings Hamiltonian
+ H = ωc * a' * a + ωq / 2 * σz + g * (a' * σm + a * σm')
+ ψ0 = kron(fock(N, 0), fock(2, 0))
+
+ e_ops = [a' * a, σz]
+ c_ops = [sqrt(γ * (1 + nth)) * a, sqrt(γ * nth) * a', sqrt(γ * (1 + nth)) * σm, sqrt(γ * nth) * σm']
+
+ sme_η = 0.7 # Efficiency of the homodyne detector for smesolve
+ c_ops_sme = [sqrt(1 - sme_η) * op for op in c_ops]
+ sc_ops_sme = [sqrt(sme_η) * op for op in c_ops]
+
+ # The following definition is to test the case of `sc_ops` as an `AbstractQuantumObject`
+ c_ops_sme2 = c_ops[2:end]
+ sc_ops_sme2 = c_ops[1]
+
+ ψ0_int = Qobj(round.(Int, real.(ψ0.data)), dims = ψ0.dims) # Used for testing the type inference
+
+ ψ_wrong = kron(fock(N - 1, 0), fock(2, 0))
+
+ rng = MersenneTwister(12)
+
+ # QobjEvo
+ ωd = 1.02
+ F = 0.05
+ coef1(p, t) = p.F * exp(1im * p.ωd * t)
+ coef2(p, t) = p.F * exp(-1im * p.ωd * t)
+ p = (F = F, ωd = ωd)
+ H_td = (H, (a, coef1), (a', coef2))
+ H_td2 = QobjEvo(H_td)
+ L_td = liouvillian(H_td2)
+
+ # time list and saveat
+ tlist = range(0, 10 / γ, 100)
+ saveat_idxs = 50:90
+ saveat = tlist[saveat_idxs]
+
+ # time list for testing exceptions
+ tlist1 = Float64[]
+ tlist2 = [0, 0.2, 0.1]
+ tlist3 = [0, 0.1, 0.1, 0.2]
+
+ # mesolve solution used for comparing results from mcsolve, ssesolve, and smesolve with mesolve
+ prob_me = mesolveProblem(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ sol_me = mesolve(prob_me)
+end
+
+@testitem "sesolve" setup=[TESetup] begin
+ using SciMLOperators
+
+ # Get parameters from TESetup to simplify the code
+ H = TESetup.H
+ ψ0 = TESetup.ψ0
+ e_ops = TESetup.e_ops
+
+ tlist = range(0, 20 * 2π / TESetup.g, 1000)
+ saveat_idxs = 500:900
+ saveat = tlist[saveat_idxs]
+
+ prob = sesolveProblem(H, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false))
+ sol = sesolve(prob)
+ sol2 = sesolve(H, ψ0, tlist, progress_bar = Val(false))
+ sol3 = sesolve(H, ψ0, tlist, e_ops = e_ops, saveat = saveat, progress_bar = Val(false))
+
+ ## Analytical solution for the expectation value of a' * a
+ Ω_rabi = sqrt(TESetup.g^2 + ((TESetup.ωc - TESetup.ωq) / 2)^2)
+ amp_rabi = TESetup.g^2 / Ω_rabi^2
+ ##
+
+ @test prob.prob.f.f isa MatrixOperator
+ @test sum(abs.(sol.expect[1, :] .- amp_rabi .* sin.(Ω_rabi * tlist) .^ 2)) / length(tlist) < 0.1
+ @test length(sol.times) == length(tlist)
+ @test length(sol.times_states) == 1
+ @test length(sol.states) == 1
+ @test size(sol.expect) == (length(e_ops), length(tlist))
+ @test length(sol2.times) == length(tlist)
+ @test length(sol2.times_states) == length(tlist)
+ @test length(sol2.states) == length(tlist)
+ @test sol2.expect === nothing
+ @test length(sol3.times) == length(tlist)
+ @test length(sol3.times_states) == length(saveat)
+ @test length(sol3.states) == length(saveat)
+ @test size(sol3.expect) == (length(e_ops), length(tlist))
+ @test sol.expect[1, saveat_idxs] ≈ expect(e_ops[1], sol3.states) atol = 1e-6
+
+ sol_string = sprint((t, s) -> show(t, "text/plain", s), sol)
+ @test sol_string ==
+ "Solution of time evolution\n" *
+ "(return code: $(sol.retcode))\n" *
+ "--------------------------\n" *
+ "num_states = $(length(sol.states))\n" *
+ "num_expect = $(size(sol.expect, 1))\n" *
+ "ODE alg.: $(sol.alg)\n" *
+ "abstol = $(sol.abstol)\n" *
+ "reltol = $(sol.reltol)\n"
+
+ sol_string2 = sprint((t, s) -> show(t, "text/plain", s), sol2)
+ @test sol_string2 ==
+ "Solution of time evolution\n" *
+ "(return code: $(sol2.retcode))\n" *
+ "--------------------------\n" *
+ "num_states = $(length(sol2.states))\n" *
+ "num_expect = 0\n" *
+ "ODE alg.: $(sol2.alg)\n" *
+ "abstol = $(sol2.abstol)\n" *
+ "reltol = $(sol2.reltol)\n"
+
+ @test_throws ArgumentError sesolve(H, ψ0, TESetup.tlist1, progress_bar = Val(false))
+ @test_throws ArgumentError sesolve(H, ψ0, TESetup.tlist2, progress_bar = Val(false))
+ @test_throws ArgumentError sesolve(H, ψ0, TESetup.tlist3, progress_bar = Val(false))
+ @test_throws ArgumentError sesolve(H, ψ0, tlist, save_idxs = [1, 2], progress_bar = Val(false))
+ @test_throws DimensionMismatch sesolve(H, TESetup.ψ_wrong, tlist, progress_bar = Val(false))
+
+ @testset "Memory Allocations" begin
+ allocs_tot = @allocations sesolve(H, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false)) # Warm-up
+ allocs_tot = @allocations sesolve(H, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false))
+ @test allocs_tot < 110
+
+ allocs_tot = @allocations sesolve(H, ψ0, tlist, saveat = [tlist[end]], progress_bar = Val(false)) # Warm-up
+ allocs_tot = @allocations sesolve(H, ψ0, tlist, saveat = [tlist[end]], progress_bar = Val(false))
+ @test allocs_tot < 90
+ end
+
+ @testset "Type Inference sesolve" begin
+ @inferred sesolveProblem(H, ψ0, tlist, progress_bar = Val(false))
+ @inferred sesolveProblem(H, ψ0, [0, 10], progress_bar = Val(false))
+ @inferred sesolveProblem(H, TESetup.ψ0_int, tlist, progress_bar = Val(false))
+ @inferred sesolve(H, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false))
+ @inferred sesolve(H, ψ0, tlist, progress_bar = Val(false))
+ @inferred sesolve(H, ψ0, tlist, e_ops = e_ops, saveat = saveat, progress_bar = Val(false))
+ @inferred sesolve(H, ψ0, tlist, e_ops = (TESetup.a' * TESetup.a, TESetup.a'), progress_bar = Val(false)) # We test the type inference for Tuple of different types
+ end
+end
+
+@testitem "mesolve" setup=[TESetup] begin
+ using SciMLOperators
+
+ # Get parameters from TESetup to simplify the code
+ H = TESetup.H
+ ψ0 = TESetup.ψ0
+ tlist = TESetup.tlist
+ c_ops = TESetup.c_ops
+ e_ops = TESetup.e_ops
+ saveat = TESetup.saveat
+ sol_me = TESetup.sol_me
+
+ sol_me2 = mesolve(H, ψ0, tlist, c_ops, progress_bar = Val(false))
+ sol_me3 = mesolve(H, ψ0, tlist, c_ops, e_ops = e_ops, saveat = saveat, progress_bar = Val(false))
+
+ # For testing the `OperatorKet` input
+ sol_me4 = mesolve(H, operator_to_vector(ket2dm(ψ0)), tlist, c_ops, saveat = saveat, progress_bar = Val(false))
+
+ # Redirect to `sesolve`
+ sol_me5 = mesolve(H, ψ0, tlist, progress_bar = Val(false))
+
+ @test TESetup.prob_me.prob.f.f isa MatrixOperator
+ @test isket(sol_me5.states[1])
+ @test length(sol_me.times) == length(tlist)
+ @test length(sol_me.times_states) == 1
+ @test length(sol_me.states) == 1
+ @test size(sol_me.expect) == (length(e_ops), length(tlist))
+ @test length(sol_me2.times) == length(tlist)
+ @test length(sol_me2.times_states) == length(tlist)
+ @test length(sol_me2.states) == length(tlist)
+ @test sol_me2.expect === nothing
+ @test length(sol_me3.times) == length(tlist)
+ @test length(sol_me3.times_states) == length(saveat)
+ @test length(sol_me3.states) == length(saveat)
+ @test size(sol_me3.expect) == (length(e_ops), length(tlist))
+ @test sol_me3.expect[1, TESetup.saveat_idxs] ≈ expect(e_ops[1], sol_me3.states) atol = 1e-6
+ @test all([sol_me3.states[i] ≈ vector_to_operator(sol_me4.states[i]) for i in eachindex(saveat)])
+
+ sol_me_string = sprint((t, s) -> show(t, "text/plain", s), sol_me)
+ @test sol_me_string ==
+ "Solution of time evolution\n" *
+ "(return code: $(sol_me.retcode))\n" *
+ "--------------------------\n" *
+ "num_states = $(length(sol_me.states))\n" *
+ "num_expect = $(size(sol_me.expect, 1))\n" *
+ "ODE alg.: $(sol_me.alg)\n" *
+ "abstol = $(sol_me.abstol)\n" *
+ "reltol = $(sol_me.reltol)\n"
+
+ @test_throws ArgumentError mesolve(H, ψ0, TESetup.tlist1, c_ops, progress_bar = Val(false))
+ @test_throws ArgumentError mesolve(H, ψ0, TESetup.tlist2, c_ops, progress_bar = Val(false))
+ @test_throws ArgumentError mesolve(H, ψ0, TESetup.tlist3, c_ops, progress_bar = Val(false))
+ @test_throws ArgumentError mesolve(H, ψ0, tlist, c_ops, save_idxs = [1, 2], progress_bar = Val(false))
+ @test_throws DimensionMismatch mesolve(H, TESetup.ψ_wrong, tlist, c_ops, progress_bar = Val(false))
+
+ @testset "Memory Allocations (mesolve)" begin
+ a = TESetup.a
+ p = TESetup.p
+
+ # We predefine the Liouvillian to avoid to count the allocations of the liouvillian function
+ L = liouvillian(H, c_ops)
+ L_td = QobjEvo((liouvillian(H, c_ops), (liouvillian(a), TESetup.coef1), (liouvillian(a'), TESetup.coef2)))
+
+ allocs_tot = @allocations mesolve(L, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false)) # Warm-up
+ allocs_tot = @allocations mesolve(L, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false))
+ @test allocs_tot < 180
+
+ allocs_tot = @allocations mesolve(L, ψ0, tlist, saveat = [tlist[end]], progress_bar = Val(false)) # Warm-up
+ allocs_tot = @allocations mesolve(L, ψ0, tlist, saveat = [tlist[end]], progress_bar = Val(false))
+ @test allocs_tot < 110
+
+ allocs_tot = @allocations mesolve(L_td, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false), params = p) # Warm-up
+ allocs_tot = @allocations mesolve(L_td, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false), params = p)
+ @test allocs_tot < 180
+
+ allocs_tot = @allocations mesolve(L_td, ψ0, tlist, progress_bar = Val(false), saveat = [tlist[end]], params = p) # Warm-up
+ allocs_tot = @allocations mesolve(L_td, ψ0, tlist, progress_bar = Val(false), saveat = [tlist[end]], params = p)
+ @test allocs_tot < 110
+ end
+
+ @testset "Type Inference (mesolve)" begin
+ a = TESetup.a
+ p = TESetup.p
+
+ coef(p, t) = exp(-t)
+ ad_t = QobjEvo(a', coef)
+ @inferred mesolveProblem(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ @inferred mesolveProblem(H, ψ0, [0, 10], c_ops, e_ops = e_ops, progress_bar = Val(false))
+ @inferred mesolveProblem(H, TESetup.ψ0_int, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ @inferred mesolve(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ @inferred mesolve(H, ψ0, tlist, c_ops, progress_bar = Val(false))
+ @inferred mesolve(H, ψ0, tlist, c_ops, e_ops = e_ops, saveat = tlist, progress_bar = Val(false))
+ @inferred mesolve(H, ψ0, tlist, (a, ad_t), e_ops = (a' * a, a'), progress_bar = Val(false)) # We test the type inference for Tuple
+ @inferred mesolve(TESetup.H_td, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p)
+ @inferred mesolve(TESetup.H_td2, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p)
+ @inferred mesolve(TESetup.L_td, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p)
+ end
+end
+
+@testitem "mcsolve" setup=[TESetup] begin
+ using SciMLOperators
+ using Statistics
+
+ # Get parameters from TESetup to simplify the code
+ H = TESetup.H
+ ψ0 = TESetup.ψ0
+ tlist = TESetup.tlist
+ c_ops = TESetup.c_ops
+ e_ops = TESetup.e_ops
+ saveat = TESetup.saveat
+ saveat_idxs = TESetup.saveat_idxs
+ sol_me = TESetup.sol_me
+
+ prob_mc = mcsolveProblem(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ sol_mc = mcsolve(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ sol_mc2 = mcsolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ jump_callback = DiscreteLindbladJumpCallback(),
+ )
+ sol_mc3 = mcsolve(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), keep_runs_results = Val(true))
+ sol_mc_states =
+ mcsolve(H, ψ0, tlist, c_ops, saveat = saveat, progress_bar = Val(false), keep_runs_results = Val(true))
+ sol_mc_states2 = mcsolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ saveat = saveat,
+ progress_bar = Val(false),
+ jump_callback = DiscreteLindbladJumpCallback(),
+ keep_runs_results = Val(true),
+ )
+
+ # also test function average_states
+ # average the states from all trajectories, and then calculate the expectation value
+ expect_mc_states_mean = expect.(Ref(e_ops[1]), average_states(sol_mc_states))
+ expect_mc_states_mean2 = expect.(Ref(e_ops[1]), average_states(sol_mc_states2))
+
+ @test prob_mc.prob.f.f isa MatrixOperator
+ @test sum(abs, sol_mc.expect .- sol_me.expect) / length(tlist) < 0.1
+ @test sum(abs, sol_mc2.expect .- sol_me.expect) / length(tlist) < 0.1
+ @test sum(abs, average_expect(sol_mc3) .- sol_me.expect) / length(tlist) < 0.1
+ @test sum(abs, expect_mc_states_mean .- vec(sol_me.expect[1, saveat_idxs])) / length(tlist) < 0.1
+ @test sum(abs, expect_mc_states_mean2 .- vec(sol_me.expect[1, saveat_idxs])) / length(tlist) < 0.1
+ @test length(sol_mc.times) == length(tlist)
+ @test length(sol_mc.times_states) == 1
+ @test size(sol_mc.expect) == (length(e_ops), length(tlist))
+ @test size(sol_mc.states) == (1,)
+ @test length(sol_mc3.times) == length(tlist)
+ @test length(sol_mc3.times_states) == 1
+ @test size(sol_mc3.expect) == (length(e_ops), 500, length(tlist)) # ntraj = 500
+ @test size(sol_mc3.states) == (500, 1) # ntraj = 500
+ @test length(sol_mc_states.times) == length(tlist)
+ @test length(sol_mc_states.times_states) == length(saveat)
+ @test size(sol_mc_states.states) == (500, length(saveat)) # ntraj = 500
+ @test sol_mc_states.expect === nothing
+
+ sol_mc_string = sprint((t, s) -> show(t, "text/plain", s), sol_mc)
+ sol_mc_string_states = sprint((t, s) -> show(t, "text/plain", s), sol_mc_states)
+ @test sol_mc_string ==
+ "Solution of quantum trajectories\n" *
+ "(converged: $(sol_mc.converged))\n" *
+ "--------------------------------\n" *
+ "num_trajectories = $(sol_mc.ntraj)\n" *
+ "num_states = $(size(sol_mc.states, ndims(sol_mc.states)))\n" *
+ "num_expect = $(size(sol_mc.expect, 1))\n" *
+ "ODE alg.: $(sol_mc.alg)\n" *
+ "abstol = $(sol_mc.abstol)\n" *
+ "reltol = $(sol_mc.reltol)\n"
+ @test sol_mc_string_states ==
+ "Solution of quantum trajectories\n" *
+ "(converged: $(sol_mc_states.converged))\n" *
+ "--------------------------------\n" *
+ "num_trajectories = $(sol_mc_states.ntraj)\n" *
+ "num_states = $(size(sol_mc_states.states, ndims(sol_mc_states.states)))\n" *
+ "num_expect = 0\n" *
+ "ODE alg.: $(sol_mc_states.alg)\n" *
+ "abstol = $(sol_mc_states.abstol)\n" *
+ "reltol = $(sol_mc_states.reltol)\n"
+
+ @test_throws ArgumentError mcsolve(H, ψ0, TESetup.tlist1, c_ops, progress_bar = Val(false))
+ @test_throws ArgumentError mcsolve(H, ψ0, TESetup.tlist2, c_ops, progress_bar = Val(false))
+ @test_throws ArgumentError mcsolve(H, ψ0, TESetup.tlist3, c_ops, progress_bar = Val(false))
+ @test_throws ArgumentError mcsolve(H, ψ0, tlist, c_ops, save_idxs = [1, 2], progress_bar = Val(false))
+ @test_throws DimensionMismatch mcsolve(H, TESetup.ψ_wrong, tlist, c_ops, progress_bar = Val(false))
+
+ # test average_states, average_expect, and std_expect
+ expvals_all = sol_mc3.expect[:, :, 2:end] # ignore testing initial time point since its standard deviation is a very small value (basically zero)
+ stdvals = std_expect(sol_mc3)
+ @test average_states(sol_mc) == sol_mc.states
+ @test average_expect(sol_mc) == sol_mc.expect
+ @test size(stdvals) == (length(e_ops), length(tlist))
+ @test all(
+ isapprox.(
+ stdvals[:, 2:end], # ignore testing initial time point since its standard deviation is a very small value (basically zero)
+ dropdims(sqrt.(mean(abs2.(expvals_all), dims = 2) .- abs2.(mean(expvals_all, dims = 2))), dims = 2);
+ atol = 1e-6,
+ ),
+ )
+ @test average_expect(sol_mc_states) === nothing
+ @test std_expect(sol_mc_states) === nothing
+ @test_throws ArgumentError std_expect(sol_mc)
+
+ @testset "Memory Allocations (mcsolve)" begin
+ ntraj = 100
+ for keep_runs_results in (Val(false), Val(true))
+ n1 = QuantumToolbox.getVal(keep_runs_results) ? 120 : 140
+ n2 = QuantumToolbox.getVal(keep_runs_results) ? 110 : 130
+
+ allocs_tot = @allocations mcsolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ e_ops = e_ops,
+ ntraj = ntraj,
+ progress_bar = Val(false),
+ keep_runs_results = Val(true),
+ ) # Warm-up
+ allocs_tot = @allocations mcsolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ e_ops = e_ops,
+ ntraj = ntraj,
+ progress_bar = Val(false),
+ keep_runs_results = Val(true),
+ )
+ @test allocs_tot < n1 * ntraj + 400 # 150 allocations per trajectory + 500 for initialization
+
+ allocs_tot = @allocations mcsolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = ntraj,
+ saveat = [tlist[end]],
+ progress_bar = Val(false),
+ keep_runs_results = Val(true),
+ ) # Warm-up
+ allocs_tot = @allocations mcsolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = ntraj,
+ saveat = [tlist[end]],
+ progress_bar = Val(false),
+ keep_runs_results = Val(true),
+ )
+ @test allocs_tot < n2 * ntraj + 300 # 100 allocations per trajectory + 300 for initialization
+ end
+ end
+
+ @testset "Type Inference (mcsolve)" begin
+ a = TESetup.a
+ rng = TESetup.rng
+
+ @inferred mcsolveEnsembleProblem(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = 5,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ )
+ @inferred mcsolve(H, ψ0, tlist, c_ops, ntraj = 5, e_ops = e_ops, progress_bar = Val(false), rng = rng)
+ @inferred mcsolve(H, ψ0, tlist, c_ops, ntraj = 5, progress_bar = Val(true), rng = rng)
+ @inferred mcsolve(H, ψ0, [0, 10], c_ops, ntraj = 5, progress_bar = Val(false), rng = rng)
+ @inferred mcsolve(H, TESetup.ψ0_int, tlist, c_ops, ntraj = 5, progress_bar = Val(false), rng = rng)
+ @inferred mcsolve(H, ψ0, tlist, (a, a'), e_ops = (a' * a, a'), ntraj = 5, progress_bar = Val(false), rng = rng) # We test the type inference for Tuple of different types
+ @inferred mcsolve(
+ TESetup.H_td,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = 5,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ params = TESetup.p,
+ rng = rng,
+ )
+ end
+end
+
+@testitem "ssesolve" setup=[TESetup] begin
+ # Get parameters from TESetup to simplify the code
+ H = TESetup.H
+ ψ0 = TESetup.ψ0
+ tlist = TESetup.tlist
+ c_ops = TESetup.c_ops
+ e_ops = TESetup.e_ops
+ sol_me = TESetup.sol_me
+
+ sol_sse = ssesolve(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ sol_sse2 = ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ e_ops = e_ops,
+ ntraj = 20,
+ progress_bar = Val(false),
+ store_measurement = Val(true),
+ )
+
+ @test sum(abs, sol_sse.expect .- sol_me.expect) / length(tlist) < 0.1
+ @test length(sol_sse.times) == length(tlist)
+ @test length(sol_sse.times_states) == 1
+ @test size(sol_sse.states) == (1,) # ntraj = 500 but keep_runs_results = Val(false)
+ @test size(sol_sse.expect) == (length(e_ops), length(tlist))
+ @test isnothing(sol_sse.measurement)
+ @test size(sol_sse2.measurement) == (length(c_ops), 20, length(tlist) - 1)
+
+ sol_sse_string = sprint((t, s) -> show(t, "text/plain", s), sol_sse)
+ @test sol_sse_string ==
+ "Solution of stochastic quantum trajectories\n" *
+ "(converged: $(sol_sse.converged))\n" *
+ "--------------------------------\n" *
+ "num_trajectories = $(sol_sse.ntraj)\n" *
+ "num_states = $(size(sol_sse.states, ndims(sol_sse.states)))\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"
+
+ @test_throws ArgumentError ssesolve(H, ψ0, TESetup.tlist1, c_ops, progress_bar = Val(false))
+ @test_throws ArgumentError ssesolve(H, ψ0, TESetup.tlist2, c_ops, progress_bar = Val(false))
+ @test_throws ArgumentError ssesolve(H, ψ0, TESetup.tlist3, c_ops, progress_bar = Val(false))
+
+ @testset "Memory Allocations (ssesolve)" begin
+ ntraj = 100
+ for keep_runs_results in (Val(false), Val(true))
+ n1 = QuantumToolbox.getVal(keep_runs_results) ? 1100 : 1120
+ n2 = QuantumToolbox.getVal(keep_runs_results) ? 1000 : 1020
+
+ allocs_tot = @allocations ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ e_ops = e_ops,
+ ntraj = ntraj,
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ ) # Warm-up
+ allocs_tot = @allocations ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ e_ops = e_ops,
+ ntraj = ntraj,
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ )
+ @test allocs_tot < n1 * ntraj + 400 # TODO: Fix this high number of allocations
+
+ allocs_tot = @allocations ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = ntraj,
+ saveat = [tlist[end]],
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ ) # Warm-up
+ allocs_tot = @allocations ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = ntraj,
+ saveat = [tlist[end]],
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ )
+ @test allocs_tot < n2 * ntraj + 300 # TODO: Fix this high number of allocations
+ end
+ end
+
+ @testset "Type Inference (ssesolve)" begin
+ a = TESetup.a
+ rng = TESetup.rng
+ p = TESetup.p
+
+ c_ops_tuple = Tuple(c_ops) # To avoid type instability, we must have a Tuple instead of a Vector
+ @inferred ssesolveEnsembleProblem(
+ H,
+ ψ0,
+ tlist,
+ c_ops_tuple,
+ ntraj = 5,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ )
+ @inferred ssesolve(H, ψ0, tlist, c_ops_tuple, ntraj = 5, e_ops = e_ops, progress_bar = Val(false), rng = rng)
+ @inferred ssesolve(H, ψ0, tlist, c_ops_tuple, ntraj = 5, progress_bar = Val(true), rng = rng)
+ @inferred ssesolve(H, ψ0, [0, 10], c_ops_tuple, ntraj = 5, progress_bar = Val(false), rng = rng)
+ @inferred ssesolve(H, TESetup.ψ0_int, tlist, c_ops_tuple, ntraj = 5, progress_bar = Val(false), rng = rng)
+ @inferred ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_tuple,
+ ntraj = 5,
+ e_ops = (a' * a, a'),
+ progress_bar = Val(false),
+ rng = rng,
+ ) # We test the type inference for Tuple of different types
+ @inferred ssesolve(
+ TESetup.H_td,
+ ψ0,
+ tlist,
+ c_ops_tuple,
+ ntraj = 5,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ params = p,
+ rng = rng,
+ )
+ end
+end
+
+@testitem "smesolve" setup=[TESetup] begin
+ using Random
+
+ # Get parameters from TESetup to simplify the code
+ H = TESetup.H
+ ψ0 = TESetup.ψ0
+ tlist = TESetup.tlist
+ c_ops_sme = TESetup.c_ops_sme
+ sc_ops_sme = TESetup.sc_ops_sme
+ c_ops_sme2 = TESetup.c_ops_sme2
+ sc_ops_sme2 = TESetup.sc_ops_sme2
+ e_ops = TESetup.e_ops
+ sol_me = TESetup.sol_me
+ saveat = TESetup.saveat
+
+ sol_sme = smesolve(H, ψ0, tlist, c_ops_sme, sc_ops_sme, e_ops = e_ops, progress_bar = Val(false))
+ sol_sme2 = smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ e_ops = e_ops,
+ ntraj = 20,
+ progress_bar = Val(false),
+ store_measurement = Val(true),
+ )
+ sol_sme3 = smesolve(H, ψ0, tlist, c_ops_sme2, sc_ops_sme2, e_ops = e_ops, progress_bar = Val(false))
+
+ # For testing the `OperatorKet` input
+ sol_sme4 = smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ saveat = saveat,
+ ntraj = 10,
+ progress_bar = Val(false),
+ rng = MersenneTwister(12),
+ )
+ sol_sme5 = smesolve(
+ H,
+ operator_to_vector(ket2dm(ψ0)),
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ saveat = saveat,
+ ntraj = 10,
+ progress_bar = Val(false),
+ rng = MersenneTwister(12),
+ )
+
+ @test sum(abs, sol_sme.expect .- sol_me.expect) / length(tlist) < 0.1
+ @test sum(abs, sol_sme3.expect .- sol_me.expect) / length(tlist) < 0.1
+ @test length(sol_sme.times) == length(tlist)
+ @test length(sol_sme.times_states) == 1
+ @test size(sol_sme.states) == (1,) # ntraj = 500 but keep_runs_results = Val(false)
+ @test size(sol_sme.expect) == (length(e_ops), length(tlist))
+ @test isnothing(sol_sme.measurement)
+ @test size(sol_sme2.measurement) == (length(sc_ops_sme), 20, length(tlist) - 1)
+ @test all([sol_sme4.states[i] ≈ vector_to_operator(sol_sme5.states[i]) for i in eachindex(saveat)])
+
+ sol_sme_string = sprint((t, s) -> show(t, "text/plain", s), sol_sme)
+ @test sol_sme_string ==
+ "Solution of stochastic quantum trajectories\n" *
+ "(converged: $(sol_sme.converged))\n" *
+ "--------------------------------\n" *
+ "num_trajectories = $(sol_sme.ntraj)\n" *
+ "num_states = $(size(sol_sme.states, ndims(sol_sme.states)))\n" *
+ "num_expect = $(size(sol_sme.expect, 1))\n" *
+ "SDE alg.: $(sol_sme.alg)\n" *
+ "abstol = $(sol_sme.abstol)\n" *
+ "reltol = $(sol_sme.reltol)\n"
+
+ @test_throws ArgumentError smesolve(H, ψ0, TESetup.tlist1, c_ops_sme, sc_ops_sme, progress_bar = Val(false))
+ @test_throws ArgumentError smesolve(H, ψ0, TESetup.tlist2, c_ops_sme, sc_ops_sme, progress_bar = Val(false))
+ @test_throws ArgumentError smesolve(H, ψ0, TESetup.tlist3, c_ops_sme, sc_ops_sme, progress_bar = Val(false))
+
+ @testset "Memory Allocations (smesolve)" begin
+ ntraj = 100
+ for keep_runs_results in (Val(false), Val(true))
+ n1 = QuantumToolbox.getVal(keep_runs_results) ? 1100 : 1120
+ n2 = QuantumToolbox.getVal(keep_runs_results) ? 1000 : 1020
+ n3 = QuantumToolbox.getVal(keep_runs_results) ? 600 : 620
+ n4 = QuantumToolbox.getVal(keep_runs_results) ? 550 : 570
+
+ allocs_tot = @allocations smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ e_ops = e_ops,
+ ntraj = ntraj,
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ ) # Warm-up
+ allocs_tot = @allocations smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ e_ops = e_ops,
+ ntraj = ntraj,
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ )
+ @test allocs_tot < n1 * ntraj + 2300 # TODO: Fix this high number of allocations
+
+ allocs_tot = @allocations smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ ntraj = ntraj,
+ saveat = [tlist[end]],
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ ) # Warm-up
+ allocs_tot = @allocations smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ ntraj = ntraj,
+ saveat = [tlist[end]],
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ )
+ @test allocs_tot < n2 * ntraj + 1500 # TODO: Fix this high number of allocations
+
+ # Diagonal Noise Case
+ allocs_tot = @allocations smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme2,
+ sc_ops_sme2,
+ e_ops = e_ops,
+ ntraj = ntraj,
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ ) # Warm-up
+ allocs_tot = @allocations smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme2,
+ sc_ops_sme2,
+ e_ops = e_ops,
+ ntraj = 1,
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ )
+ @test allocs_tot < n3 * ntraj + 1400 # TODO: Fix this high number of allocations
+
+ allocs_tot = @allocations smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme2,
+ sc_ops_sme2,
+ ntraj = ntraj,
+ saveat = [tlist[end]],
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ ) # Warm-up
+ allocs_tot = @allocations smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme2,
+ sc_ops_sme2,
+ ntraj = 1,
+ saveat = [tlist[end]],
+ progress_bar = Val(false),
+ keep_runs_results = keep_runs_results,
+ )
+ @test allocs_tot < n4 * ntraj + 1000 # TODO: Fix this high number of allocations
+ end
+ end
+
+ @testset "Type Inference (smesolve)" begin
+ a = TESetup.a
+ rng = TESetup.rng
+
+ # To avoid type instability, we must have a Tuple instead of a Vector
+ c_ops_sme_tuple = Tuple(c_ops_sme)
+ sc_ops_sme_tuple = Tuple(sc_ops_sme)
+ c_ops_sme2_tuple = Tuple(c_ops_sme2)
+ sc_ops_sme2_tuple = sc_ops_sme2 # This is an `AbstractQuantumObject`
+ @inferred smesolveEnsembleProblem(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme_tuple,
+ sc_ops_sme_tuple,
+ ntraj = 5,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ )
+ @inferred smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme_tuple,
+ sc_ops_sme_tuple,
+ ntraj = 5,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ )
+ @inferred smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme2_tuple,
+ sc_ops_sme2_tuple,
+ ntraj = 5,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ )
+ @inferred smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme_tuple,
+ sc_ops_sme_tuple,
+ ntraj = 5,
+ progress_bar = Val(true),
+ rng = rng,
+ )
+ @inferred smesolve(
+ H,
+ ψ0,
+ [0, 10],
+ c_ops_sme_tuple,
+ sc_ops_sme_tuple,
+ ntraj = 5,
+ progress_bar = Val(false),
+ rng = rng,
+ )
+ @inferred smesolve(
+ H,
+ TESetup.ψ0_int,
+ tlist,
+ c_ops_sme_tuple,
+ sc_ops_sme_tuple,
+ ntraj = 5,
+ progress_bar = Val(false),
+ rng = rng,
+ )
+ @inferred smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme_tuple,
+ sc_ops_sme_tuple,
+ ntraj = 5,
+ e_ops = (a' * a, a'),
+ progress_bar = Val(false),
+ rng = rng,
+ ) # We test the type inference for Tuple of different types
+ end
+end
+
+@testitem "Time-dependent Hamiltonian" setup=[TESetup] begin
+ # Get parameters from TESetup to simplify the code
+ ωd = TESetup.ωd
+ F = TESetup.F
+ a = TESetup.a
+ σz = TESetup.σz
+ H = TESetup.H
+ H_td = TESetup.H_td
+ H_td2 = TESetup.H_td2
+ L_td = TESetup.L_td
+ ψ0 = TESetup.ψ0
+ c_ops = TESetup.c_ops
+ e_ops = TESetup.e_ops
+ p = TESetup.p
+ rng = TESetup.rng
+
+ # ssesolve is slow to be run on CI. It is not removed from the test because it may be useful for testing in more powerful machines.
+
+ # Time Evolution in the drive frame
+
+ H_dr_fr = H - ωd * a' * a - ωd * σz / 2 + F * (a + a')
+
+ tlist = range(0, 10 / TESetup.γ, 1000)
+
+ sol_se = sesolve(H_dr_fr, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false))
+ sol_me = mesolve(H_dr_fr, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false))
+ sol_mc = mcsolve(H_dr_fr, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), rng = rng)
+ # sol_sse = ssesolve(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), rng = rng)
+
+ # Time Evolution in the lab frame
+
+ sol_se_td = sesolve(H_td, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false), params = p)
+ sol_me_td = mesolve(H_td, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p)
+ sol_mc_td = mcsolve(H_td, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p, rng = rng)
+ # sol_sse_td = ssesolve(H_td, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p, rng = rng)
+
+ @test sol_se.expect ≈ sol_se_td.expect atol = 1e-6 * length(tlist)
+ @test sol_me.expect ≈ sol_me_td.expect atol = 1e-6 * length(tlist)
+ @test sol_mc.expect ≈ sol_mc_td.expect atol = 1e-2 * length(tlist)
+ # @test sol_sse.expect ≈ sol_sse_td.expect atol = 1e-2 * length(tlist)
+
+ sol_se_td2 = sesolve(H_td2, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false), params = p)
+ sol_me_td2 = mesolve(L_td, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p)
+ sol_mc_td2 = mcsolve(H_td2, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p, rng = rng)
+ # sol_sse_td2 =
+ # ssesolve(H_td2, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), params = p, rng = rng)
+
+ @test sol_se.expect ≈ sol_se_td2.expect atol = 1e-6 * length(tlist)
+ @test sol_me.expect ≈ sol_me_td2.expect atol = 1e-6 * length(tlist)
+ @test sol_mc.expect ≈ sol_mc_td2.expect atol = 1e-2 * length(tlist)
+ # @test sol_sse.expect ≈ sol_sse_td2.expect atol = 1e-2 * length(tlist)
+end
+
+@testitem "mcsolve, ssesolve and smesolve reproducibility" setup=[TESetup] begin
+ using Random
+
+ # Get parameters from TESetup to simplify the code
+ H = TESetup.H
+ ψ0 = TESetup.ψ0
+ tlist = TESetup.tlist
+ c_ops = TESetup.c_ops
+ c_ops_sme = TESetup.c_ops_sme
+ sc_ops_sme = TESetup.sc_ops_sme
+ e_ops = TESetup.e_ops
+ rng = TESetup.rng
+
+ rng = MersenneTwister(1234)
+ sol_mc1 =
+ mcsolve(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), rng = rng, keep_runs_results = Val(true))
+ rng = MersenneTwister(1234)
+ sol_sse1 = ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = 50,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ keep_runs_results = Val(true),
+ )
+ rng = MersenneTwister(1234)
+ sol_sme1 = smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ ntraj = 50,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ keep_runs_results = Val(true),
+ )
+
+ rng = MersenneTwister(1234)
+ sol_mc2 =
+ mcsolve(H, ψ0, tlist, c_ops, e_ops = e_ops, progress_bar = Val(false), rng = rng, keep_runs_results = Val(true))
+ rng = MersenneTwister(1234)
+ sol_sse2 = ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = 50,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ keep_runs_results = Val(true),
+ )
+ rng = MersenneTwister(1234)
+ sol_sme2 = smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ ntraj = 50,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ keep_runs_results = Val(true),
+ )
+
+ rng = MersenneTwister(1234)
+ sol_mc3 = mcsolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = 510,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ keep_runs_results = Val(true),
+ )
+ rng = MersenneTwister(1234)
+ sol_sse3 = ssesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops,
+ ntraj = 60,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ keep_runs_results = Val(true),
+ )
+ rng = MersenneTwister(1234)
+ sol_sme3 = smesolve(
+ H,
+ ψ0,
+ tlist,
+ c_ops_sme,
+ sc_ops_sme,
+ ntraj = 60,
+ e_ops = e_ops,
+ progress_bar = Val(false),
+ rng = rng,
+ keep_runs_results = Val(true),
+ )
+
+ @test sol_mc1.expect ≈ sol_mc2.expect atol = 1e-10
+ @test sol_mc1.col_times ≈ sol_mc2.col_times atol = 1e-10
+ @test sol_mc1.col_which ≈ sol_mc2.col_which atol = 1e-10
+
+ @test sol_mc1.expect ≈ sol_mc3.expect[:, 1:500, :] atol = 1e-10
+
+ @test sol_sse1.expect ≈ sol_sse2.expect atol = 1e-10
+
+ @test sol_sse1.expect ≈ sol_sse3.expect[:, 1:50, :] atol = 1e-10
+
+ @test sol_sme1.expect ≈ sol_sme2.expect atol = 1e-10
+
+ @test sol_sme1.expect ≈ sol_sme3.expect[:, 1:50, :] atol = 1e-10
+end
+
+@testitem "example" begin
+ sp1 = kron(sigmap(), qeye(2))
+ sm1 = sp1'
+ sx1 = sm1 + sp1
+ sy1 = 1im * (sm1 - sp1)
+ sz1 = sp1 * sm1 - sm1 * sp1
+ sp2 = kron(qeye(2), sigmap())
+ sm2 = sp2'
+ sx2 = sm2 + sp2
+ sy2 = 1im * (sm2 - sp2)
+ sz2 = sp2 * sm2 - sm2 * sp2
+ ωq1, ωq2 = 1, 1
+ γ1, γ2 = 0.05, 0.1
+ H = 0.5 * ωq1 * sz1 + 0.5 * ωq2 * sz2
+ c_ops = [sqrt(γ1) * sm1, sqrt(γ2) * sm2]
+ psi0_1 = normalize(fock(2, 0) + fock(2, 1))
+ psi0_2 = normalize(fock(2, 0) + fock(2, 1))
+ psi0 = kron(psi0_1, psi0_2)
+ t_l = LinRange(0, 20 / γ1, 1000)
+ sol_me = mesolve(H, psi0, t_l, c_ops, e_ops = [sp1 * sm1, sp2 * sm2], progress_bar = false) # Here we don't put Val(false) because we want to test the support for Bool type
+ sol_mc = mcsolve(H, psi0, t_l, c_ops, e_ops = [sp1 * sm1, sp2 * sm2], progress_bar = Val(false))
+ @test sum(abs.(sol_mc.expect[1:2, :] .- sol_me.expect[1:2, :])) / length(t_l) < 0.1
+ @test expect(sp1 * sm1, sol_me.states[end]) ≈ expect(sigmap() * sigmam(), ptrace(sol_me.states[end], 1))
+end
diff --git a/test/core-test/utilities.jl b/test/core-test/utilities.jl
new file mode 100644
index 000000000..521d2bcda
--- /dev/null
+++ b/test/core-test/utilities.jl
@@ -0,0 +1,77 @@
+@testitem "Utilities" begin
+
+ # citation bibtex
+ io_buffer = IOBuffer()
+ QuantumToolbox.cite(io_buffer)
+ captured_output = String(take!(io_buffer))
+ @test captured_output ==
+ """@article{QuantumToolbox.jl2025,\n""" *
+ """ title = {Quantum{T}oolbox.jl: {A}n efficient {J}ulia framework for simulating open quantum systems},\n""" *
+ """ author = {Mercurio, Alberto and Huang, Yi-Te and Cai, Li-Xun and Chen, Yueh-Nan and Savona, Vincenzo and Nori, Franco},\n""" *
+ """ journal = {{Quantum}},\n""" *
+ """ issn = {2521-327X},\n""" *
+ """ publisher = {{Verein zur F{\\"{o}}rderung des Open Access Publizierens in den Quantenwissenschaften}},\n""" *
+ """ volume = {9},\n""" *
+ """ pages = {1866},\n""" *
+ """ month = sep,\n""" *
+ """ year = {2025},\n""" *
+ """ doi = {10.22331/q-2025-09-29-1866},\n""" *
+ """ url = {https://doi.org/10.22331/q-2025-09-29-1866}\n""" *
+ """}\n"""
+
+ @testset "n_thermal" begin
+ ω1 = rand(Float64)
+ ω2 = rand(Float64)
+ @test n_thermal(0, ω2) == 0.0
+ @test n_thermal(ω1, 0) == 0.0
+ @test n_thermal(ω1, -ω2) == 0.0
+ @test n_thermal(ω1, ω2) == 1 / (exp(ω1 / ω2) - 1)
+ @test typeof(n_thermal(Int32(2), Int32(3))) == Float32
+ @test typeof(n_thermal(Float32(2), Float32(3))) == Float32
+ @test typeof(n_thermal(Int64(2), Int32(3))) == Float64
+ @test typeof(n_thermal(Int32(2), Int64(3))) == Float64
+ @test typeof(n_thermal(Float64(2), Float32(3))) == Float64
+ @test typeof(n_thermal(Float32(2), Float64(3))) == Float64
+ end
+
+ @testset "CODATA Physical Constants" begin
+ c = PhysicalConstants.c
+ h = PhysicalConstants.h
+ ħ = PhysicalConstants.ħ
+ μ0 = PhysicalConstants.μ0
+ ϵ0 = PhysicalConstants.ϵ0
+
+ @test h / ħ ≈ 2 * π
+ @test μ0 / (4e-7 * π) ≈ 1.0
+ @test c^2 * μ0 * ϵ0 ≈ 1.0
+ end
+
+ @testset "convert unit" begin
+ V = 100 * rand(Float64)
+ _unit_list = [:J, :eV, :meV, :MHz, :GHz, :K, :mK]
+ for origin in _unit_list
+ for middle in _unit_list
+ for target in _unit_list
+ V_middle = convert_unit(V, origin, middle)
+ V_target = convert_unit(V_middle, middle, target)
+ V_origin = convert_unit(V_target, target, origin)
+ @test V ≈ V_origin
+ end
+ end
+ end
+ @test_throws ArgumentError convert_unit(V, :bad_unit, :J)
+ @test_throws ArgumentError convert_unit(V, :J, :bad_unit)
+ end
+
+ @testset "Type Inference" begin
+ v1 = rand(Float64)
+ @inferred n_thermal(v1, Int32(123))
+
+ _unit_list = [:J, :eV, :meV, :GHz, :mK]
+ for u1 in _unit_list
+ for u2 in _unit_list
+ @inferred convert_unit(v1, u1, u2)
+ end
+ end
+ end
+end
diff --git a/test/wigner.jl b/test/core-test/wigner.jl
similarity index 55%
rename from test/wigner.jl
rename to test/core-test/wigner.jl
index ae30b188e..58e292781 100644
--- a/test/wigner.jl
+++ b/test/core-test/wigner.jl
@@ -1,14 +1,14 @@
-@testset "Wigner" begin
+@testitem "Wigner" begin
α = 0.5 + 0.8im
ψ = coherent(30, α)
- ρ = dense_to_sparse(ket2dm(ψ), 1e-6)
+ ρ = to_sparse(ket2dm(ψ), 1e-6)
xvec = LinRange(-3, 3, 300)
yvec = LinRange(-3, 3, 300)
- wig = wigner(ψ, xvec, yvec, solver = WignerLaguerre(tol = 1e-6))
- wig2 = wigner(ρ, xvec, yvec, solver = WignerLaguerre(parallel = false))
- wig3 = wigner(ρ, xvec, yvec, solver = WignerLaguerre(parallel = true))
- wig4 = wigner(ψ, xvec, yvec, solver = WignerClenshaw())
+ wig = wigner(ψ, xvec, yvec, method = WignerLaguerre(tol = 1e-6))
+ wig2 = wigner(ρ, xvec, yvec, method = WignerLaguerre(parallel = false))
+ wig3 = wigner(ρ, xvec, yvec, method = WignerLaguerre(parallel = true))
+ wig4 = wigner(ψ, xvec, yvec, method = WignerClenshaw())
@test sqrt(sum(abs.(wig2 .- wig)) / length(wig)) < 1e-3
@test sqrt(sum(abs.(wig3 .- wig)) / length(wig)) < 1e-3
@@ -22,9 +22,9 @@
@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())
+ @inferred wigner(ψ, xvec, yvec, method = WignerLaguerre(tol = 1e-6))
+ @inferred wigner(ρ, xvec, yvec, method = WignerLaguerre(parallel = false))
+ @inferred wigner(ρ, xvec, yvec, method = WignerLaguerre(parallel = true))
+ @inferred wigner(ψ, xvec, yvec, method = WignerClenshaw())
end
end
diff --git a/test/correlations_and_spectrum.jl b/test/correlations_and_spectrum.jl
deleted file mode 100644
index 5c5079096..000000000
--- a/test/correlations_and_spectrum.jl
+++ /dev/null
@@ -1,23 +0,0 @@
-@testset "Correlations and Spectrum" begin
- a = destroy(10)
- H = a' * a
- c_ops = [sqrt(0.1 * (0.01 + 1)) * a, sqrt(0.1 * (0.01)) * a']
-
- ω_l = range(0, 3, length = 1000)
- ω_l1, spec1 = spectrum(H, ω_l, a', a, c_ops, solver = FFTCorrelation(), progress_bar = Val(false))
- ω_l2, spec2 = spectrum(H, ω_l, a', a, c_ops)
- spec1 = spec1 ./ maximum(spec1)
- spec2 = spec2 ./ maximum(spec2)
-
- test_func1 = maximum(real.(spec1)) * (0.1 / 2)^2 ./ ((ω_l1 .- 1) .^ 2 .+ (0.1 / 2)^2)
- test_func2 = maximum(real.(spec2)) * (0.1 / 2)^2 ./ ((ω_l2 .- 1) .^ 2 .+ (0.1 / 2)^2)
- idxs1 = test_func1 .> 0.05
- idxs2 = test_func2 .> 0.05
- @test sum(abs2.(spec1[idxs1] .- test_func1[idxs1])) / sum(abs2.(test_func1[idxs1])) < 0.01
- @test sum(abs2.(spec2[idxs2] .- test_func2[idxs2])) / sum(abs2.(test_func2[idxs2])) < 0.01
-
- @testset "Type Inference spectrum" begin
- @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/cuda_ext.jl b/test/cuda_ext.jl
deleted file mode 100644
index f8c7f1f8d..000000000
--- a/test/cuda_ext.jl
+++ /dev/null
@@ -1,115 +0,0 @@
-using CUDA
-using CUDA.CUSPARSE
-CUDA.allowscalar(false) # Avoid unexpected scalar indexing
-
-QuantumToolbox.about()
-CUDA.versioninfo()
-
-@testset "CUDA Extension" verbose = true begin
- ψdi = Qobj(Int64[1, 0])
- ψdf = Qobj(Float64[1, 0])
- ψdc = Qobj(ComplexF64[1, 0])
- ψsi = dense_to_sparse(ψdi)
- ψsf = dense_to_sparse(ψdf)
- ψsc = dense_to_sparse(ψdc)
-
- Xdi = Qobj(Int64[0 1; 1 0])
- Xdf = Qobj(Float64[0 1; 1 0])
- Xdc = Qobj(ComplexF64[0 1; 1 0])
- Xsi = dense_to_sparse(Xdi)
- Xsf = dense_to_sparse(Xdf)
- Xsc = dense_to_sparse(Xdc)
-
- @test_throws DomainError cu(ψdi; word_size = 16)
-
- # type conversion of CUDA dense arrays
- @test typeof(cu(ψdi; word_size = 64).data) == typeof(CuArray(ψdi).data) <: CuArray{Int64,1}
- @test typeof(cu(ψdi; word_size = 32).data) == typeof(CuArray{Int32}(ψdi).data) <: CuArray{Int32,1}
- @test typeof(cu(ψdf; word_size = 64).data) == typeof(CuArray(ψdf).data) <: CuArray{Float64,1}
- @test typeof(cu(ψdf; word_size = 32).data) == typeof(CuArray{Float32}(ψdf).data) <: CuArray{Float32,1}
- @test typeof(cu(ψdc; word_size = 64).data) == typeof(CuArray(ψdc).data) <: CuArray{ComplexF64,1}
- @test typeof(cu(ψdc; word_size = 32).data) == typeof(CuArray{ComplexF32}(ψdc).data) <: CuArray{ComplexF32,1}
- @test typeof(cu(Xdi; word_size = 64).data) == typeof(CuArray(Xdi).data) <: CuArray{Int64,2}
- @test typeof(cu(Xdi; word_size = 32).data) == typeof(CuArray{Int32}(Xdi).data) <: CuArray{Int32,2}
- @test typeof(cu(Xdf; word_size = 64).data) == typeof(CuArray(Xdf).data) <: CuArray{Float64,2}
- @test typeof(cu(Xdf; word_size = 32).data) == typeof(CuArray{Float32}(Xdf).data) <: CuArray{Float32,2}
- @test typeof(cu(Xdc; word_size = 64).data) == typeof(CuArray(Xdc).data) <: CuArray{ComplexF64,2}
- @test typeof(cu(Xdc; word_size = 32).data) == typeof(CuArray{ComplexF32}(Xdc).data) <: CuArray{ComplexF32,2}
-
- # type conversion of CUDA sparse arrays
- @test typeof(cu(ψsi; word_size = 64).data) == typeof(CuSparseVector(ψsi).data) == CuSparseVector{Int64,Int32}
- @test typeof(cu(ψsi; word_size = 32).data) == typeof(CuSparseVector{Int32}(ψsi).data) == CuSparseVector{Int32,Int32}
- @test typeof(cu(ψsf; word_size = 64).data) == typeof(CuSparseVector(ψsf).data) == CuSparseVector{Float64,Int32}
- @test typeof(cu(ψsf; word_size = 32).data) ==
- typeof(CuSparseVector{Float32}(ψsf).data) ==
- CuSparseVector{Float32,Int32}
- @test typeof(cu(ψsc; word_size = 64).data) == typeof(CuSparseVector(ψsc).data) == CuSparseVector{ComplexF64,Int32}
- @test typeof(cu(ψsc; word_size = 32).data) ==
- typeof(CuSparseVector{ComplexF32}(ψsc).data) ==
- CuSparseVector{ComplexF32,Int32}
- @test typeof(cu(Xsi; word_size = 64).data) == typeof(CuSparseMatrixCSC(Xsi).data) == CuSparseMatrixCSC{Int64,Int32}
- @test typeof(cu(Xsi; word_size = 32).data) ==
- typeof(CuSparseMatrixCSC{Int32}(Xsi).data) ==
- CuSparseMatrixCSC{Int32,Int32}
- @test typeof(cu(Xsf; word_size = 64).data) ==
- typeof(CuSparseMatrixCSC(Xsf).data) ==
- CuSparseMatrixCSC{Float64,Int32}
- @test typeof(cu(Xsf; word_size = 32).data) ==
- typeof(CuSparseMatrixCSC{Float32}(Xsf).data) ==
- CuSparseMatrixCSC{Float32,Int32}
- @test typeof(cu(Xsc; word_size = 64).data) ==
- typeof(CuSparseMatrixCSC(Xsc).data) ==
- CuSparseMatrixCSC{ComplexF64,Int32}
- @test typeof(cu(Xsc; word_size = 32).data) ==
- typeof(CuSparseMatrixCSC{ComplexF32}(Xsc).data) ==
- CuSparseMatrixCSC{ComplexF32,Int32}
- @test typeof(CuSparseMatrixCSR(Xsi).data) == CuSparseMatrixCSR{Int64,Int32}
- @test typeof(CuSparseMatrixCSR{Int32}(Xsi).data) == CuSparseMatrixCSR{Int32,Int32}
- @test typeof(CuSparseMatrixCSR(Xsf).data) == CuSparseMatrixCSR{Float64,Int32}
- @test typeof(CuSparseMatrixCSR{Float32}(Xsf).data) == CuSparseMatrixCSR{Float32,Int32}
- @test typeof(CuSparseMatrixCSR(Xsc).data) == CuSparseMatrixCSR{ComplexF64,Int32}
- @test typeof(CuSparseMatrixCSR{ComplexF32}(Xsc).data) == CuSparseMatrixCSR{ComplexF32,Int32}
-
- # brief example in README and documentation
- N = 20
- ω64 = 1.0 # Float64
- ω32 = 1.0f0 # Float32
- γ64 = 0.1 # Float64
- γ32 = 0.1f0 # Float32
- tlist = range(0, 10, 100)
-
- ## calculate by CPU
- a_cpu = destroy(N)
- ψ0_cpu = fock(N, 3)
- H_cpu = ω64 * a_cpu' * a_cpu
- sol_cpu = mesolve(H_cpu, ψ0_cpu, tlist, [sqrt(γ64) * a_cpu], e_ops = [a_cpu' * a_cpu], progress_bar = Val(false))
-
- ## calculate by GPU (with 64-bit)
- a_gpu64 = cu(destroy(N))
- ψ0_gpu64 = cu(fock(N, 3))
- H_gpu64 = ω64 * a_gpu64' * a_gpu64
- sol_gpu64 = mesolve(
- H_gpu64,
- ψ0_gpu64,
- tlist,
- [sqrt(γ64) * a_gpu64],
- e_ops = [a_gpu64' * a_gpu64],
- progress_bar = Val(false),
- )
-
- ## calculate by GPU (with 32-bit)
- a_gpu32 = cu(destroy(N), word_size = 32)
- ψ0_gpu32 = cu(fock(N, 3), word_size = 32)
- H_gpu32 = ω32 * a_gpu32' * a_gpu32
- sol_gpu32 = mesolve(
- H_gpu32,
- ψ0_gpu32,
- tlist,
- [sqrt(γ32) * a_gpu32],
- e_ops = [a_gpu32' * a_gpu32],
- progress_bar = Val(false),
- )
-
- @test all([isapprox(sol_cpu.expect[i], sol_gpu64.expect[i]) for i in 1:length(tlist)])
- @test all([isapprox(sol_cpu.expect[i], sol_gpu32.expect[i]; atol = 1e-6) for i in 1:length(tlist)])
-end
diff --git a/test/eigenvalues_and_operators.jl b/test/eigenvalues_and_operators.jl
deleted file mode 100644
index 9e8ad76a3..000000000
--- a/test/eigenvalues_and_operators.jl
+++ /dev/null
@@ -1,117 +0,0 @@
-@testset "Eigenvalues and Operators" begin
- σx = sigmax()
- result = eigenstates(σx, sparse = false)
- λd, ψd, Td = result
- resstring = sprint((t, s) -> show(t, "text/plain", s), result)
- valstring = sprint((t, s) -> show(t, "text/plain", s), result.values)
- vecsstring = sprint((t, s) -> show(t, "text/plain", s), result.vectors)
- λs, ψs, Ts = eigenstates(σx, sparse = true, k = 2)
- λs1, ψs1, Ts1 = eigenstates(σx, sparse = true, k = 1)
-
- @test all([ψ.type isa KetQuantumObject for ψ in ψd])
- @test typeof(Td) <: AbstractMatrix
- @test typeof(Ts) <: AbstractMatrix
- @test typeof(Ts1) <: AbstractMatrix
- @test all(abs.(eigenenergies(σx, sparse = false)) .≈ abs.(λd))
- @test all(abs.(eigenenergies(σx, sparse = true, k = 2)) .≈ abs.(λs))
- @test resstring ==
- "EigsolveResult: type=$(Operator) dims=$(result.dims)\nvalues:\n$(valstring)\nvectors:\n$vecsstring"
-
- N = 30
- a = kron(destroy(N), qeye(2))
- a_d = a'
-
- sm = kron(qeye(N), sigmam())
- sp = sm'
- sx = kron(qeye(N), sigmax())
- sy = kron(qeye(N), sigmay())
- sz = kron(qeye(N), sigmaz())
-
- η = 0.2
- H_d = a_d * a + 0.5 * sz - 1im * η * (a - a_d) * sx + η^2
- H_c = a_d * a + 0.5 * (sz * cosm(2 * η * (a + a_d)) + sy * sinm(2 * η * (a + a_d)))
-
- vals_d, vecs_d, mat_d = eigenstates(H_d)
- vals_c, vecs_c, mat_c = eigenstates(H_c)
- vals2, vecs2, mat2 = eigenstates(H_d, sparse = true, sigma = -0.9, k = 10, krylovdim = 30)
- sort!(vals_c, by = real)
- sort!(vals2, by = real)
-
- @test sum(real.(vals_d[1:20]) .- real.(vals_c[1:20])) / 20 < 1e-3
- @test sum(real.(vals_d[1:10]) .- real.(vals2[1:10])) / 20 < 1e-3
-
- 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)
-
- # 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(L, 1 \ (40 * κ), k = 10, krylovdim = 50)
- idxs = sortperm(vals2, by = abs)
- vals2 = vals2[idxs][1:10]
- vecs2 = vecs2[:, idxs][:, 1:10]
-
- @test isapprox(sum(abs2, vals), sum(abs2, vals2), atol = 1e-7)
- @test isapprox(abs2(vals2[1]), abs2(vals3[1]), atol = 1e-7)
- @test isapprox(vec2mat(vecs[:, 1]) * exp(-1im * angle(vecs[1, 1])), vec2mat(vecs2[:, 1]), atol = 1e-7)
- @test isapprox(vec2mat(vecs[:, 1]) * exp(-1im * angle(vecs[1, 1])), vec2mat(vecs3[:, 1]), atol = 1e-5)
-
- # eigen solve for QuantumObject
- result = eigenstates(L, sparse = true, sigma = 0.01, k = 10, krylovdim = 50)
- vals, vecs = result
- resstring = sprint((t, s) -> show(t, "text/plain", s), result)
- valstring = sprint((t, s) -> show(t, "text/plain", s), result.values)
- vecsstring = sprint((t, s) -> show(t, "text/plain", s), result.vectors)
- @test resstring ==
- "EigsolveResult: type=$(SuperOperator) dims=$(result.dims)\nvalues:\n$(valstring)\nvectors:\n$vecsstring"
-
- vals2, vecs2 = eigenstates(L, sparse = false)
- idxs = sortperm(vals2, by = abs)
- vals2 = vals2[idxs][1:10]
- vecs2 = vecs2[idxs][1:10]
-
- @test result.type isa SuperOperatorQuantumObject
- @test result.dims == L.dims
- @test all([v.type isa OperatorKetQuantumObject for v in vecs])
- @test typeof(result.vectors) <: AbstractMatrix
- @test isapprox(sum(abs2, vals), sum(abs2, vals2), atol = 1e-7)
- @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/entanglement.jl
deleted file mode 100644
index c9df3e204..000000000
--- a/test/entanglement.jl
+++ /dev/null
@@ -1,13 +0,0 @@
-@testset "Entanglement" begin
- g = fock(2, 1)
- e = fock(2, 0)
- state = normalize(kron(g, e) + kron(e, g))
- 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/ext-test/cpu/autodiff/Project.toml b/test/ext-test/cpu/autodiff/Project.toml
new file mode 100644
index 000000000..f52a45c2b
--- /dev/null
+++ b/test/ext-test/cpu/autodiff/Project.toml
@@ -0,0 +1,6 @@
+[deps]
+Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9"
+ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
+QuantumToolbox = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab"
+SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1"
+Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
diff --git a/test/ext-test/cpu/autodiff/autodiff.jl b/test/ext-test/cpu/autodiff/autodiff.jl
new file mode 100644
index 000000000..3f8e601a4
--- /dev/null
+++ b/test/ext-test/cpu/autodiff/autodiff.jl
@@ -0,0 +1,141 @@
+# ---- SESOLVE ----
+const ψ0_sesolve = fock(2, 1)
+t_max = 10
+const tlist_sesolve = range(0, t_max, 100)
+
+# For direct Forward differentiation
+function my_f_sesolve_direct(p)
+ H = p[1] * sigmax()
+ sol = sesolve(H, ψ0_sesolve, tlist_sesolve, progress_bar = Val(false))
+
+ return real(expect(projection(2, 0, 0), sol.states[end]))
+end
+
+# For SciMLSensitivity.jl
+coef_Ω(p, t) = p[1]
+const H_evo = QobjEvo(sigmax(), coef_Ω)
+
+function my_f_sesolve(p)
+ sol = sesolve(
+ H_evo,
+ ψ0_sesolve,
+ tlist_sesolve,
+ progress_bar = Val(false),
+ params = p,
+ sensealg = BacksolveAdjoint(autojacvec = EnzymeVJP()),
+ )
+
+ return real(expect(projection(2, 0, 0), sol.states[end]))
+end
+
+# Analytical solution
+my_f_analytic(Ω) = abs2(sin(Ω * t_max))
+my_f_analytic_deriv(Ω) = 2 * t_max * sin(Ω * t_max) * cos(Ω * t_max)
+
+# ---- MESOLVE ----
+const N = 20
+const a = destroy(N)
+const ψ0_mesolve = fock(N, 0)
+const tlist_mesolve = range(0, 40, 100)
+
+# For direct Forward differentiation
+function my_f_mesolve_direct(p)
+ H = p[1] * a' * a + p[2] * (a + a')
+ c_ops = [sqrt(p[3]) * a]
+ sol = mesolve(H, ψ0_mesolve, tlist_mesolve, c_ops, progress_bar = Val(false))
+ return real(expect(a' * a, sol.states[end]))
+end
+
+# For SciMLSensitivity.jl
+coef_Δ(p, t) = p[1]
+coef_F(p, t) = p[2]
+coef_γ(p, t) = sqrt(p[3])
+H = QobjEvo(a' * a, coef_Δ) + QobjEvo(a + a', coef_F)
+c_ops = [QobjEvo(a, coef_γ)]
+const L = liouvillian(H, c_ops)
+
+function my_f_mesolve(p)
+ sol = mesolve(
+ L,
+ ψ0_mesolve,
+ tlist_mesolve,
+ progress_bar = Val(false),
+ params = p,
+ sensealg = BacksolveAdjoint(autojacvec = EnzymeVJP()),
+ )
+
+ return real(expect(a' * a, sol.states[end]))
+end
+
+# Analytical solution
+n_ss(Δ, F, γ) = abs2(F / (Δ + 1im * γ / 2))
+
+@testset "Autodiff" verbose=true begin
+ @testset "sesolve" verbose=true begin
+ Ω = 1.0
+ params = [Ω]
+
+ my_f_sesolve_direct(params)
+ my_f_sesolve(params)
+
+ grad_exact = [my_f_analytic_deriv(params[1])]
+
+ @testset "ForwardDiff.jl" begin
+ grad_qt = ForwardDiff.gradient(my_f_sesolve_direct, params)
+
+ @test grad_qt ≈ grad_exact atol=1e-6
+ end
+
+ @testset "Zygote.jl" begin
+ grad_qt = Zygote.gradient(my_f_sesolve, params)[1]
+
+ @test grad_qt ≈ grad_exact atol=1e-6
+ end
+
+ @testset "Enzyme.jl" begin
+ dparams = Enzyme.make_zero(params)
+ Enzyme.autodiff(
+ Enzyme.set_runtime_activity(Enzyme.Reverse),
+ my_f_sesolve,
+ Active,
+ Duplicated(params, dparams),
+ )[1]
+
+ @test dparams ≈ grad_exact atol=1e-6
+ end
+ end
+
+ @testset "mesolve" verbose=true begin
+ Δ = 1.0
+ F = 1.0
+ γ = 1.0
+ params = [Δ, F, γ]
+
+ my_f_mesolve_direct(params)
+ my_f_mesolve(params)
+
+ grad_exact = Zygote.gradient((p) -> n_ss(p[1], p[2], p[3]), params)[1]
+
+ @testset "ForwardDiff.jl" begin
+ grad_qt = ForwardDiff.gradient(my_f_mesolve_direct, params)
+ @test grad_qt ≈ grad_exact atol=1e-6
+ end
+
+ @testset "Zygote.jl" begin
+ grad_qt = Zygote.gradient(my_f_mesolve, params)[1]
+ @test grad_qt ≈ grad_exact atol=1e-6
+ end
+
+ @testset "Enzyme.jl" begin
+ dparams = Enzyme.make_zero(params)
+ Enzyme.autodiff(
+ Enzyme.set_runtime_activity(Enzyme.Reverse),
+ my_f_mesolve,
+ Active,
+ Duplicated(params, dparams),
+ )[1]
+
+ @test dparams ≈ grad_exact atol=1e-6
+ end
+ end
+end
diff --git a/test/ext-test/cpu/makie/Project.toml b/test/ext-test/cpu/makie/Project.toml
new file mode 100644
index 000000000..0bcbd97e7
--- /dev/null
+++ b/test/ext-test/cpu/makie/Project.toml
@@ -0,0 +1,3 @@
+[deps]
+Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
+QuantumToolbox = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab"
diff --git a/test/ext-test/cpu/makie/makie_ext.jl b/test/ext-test/cpu/makie/makie_ext.jl
new file mode 100644
index 000000000..d19203735
--- /dev/null
+++ b/test/ext-test/cpu/makie/makie_ext.jl
@@ -0,0 +1,231 @@
+@testset "Makie Extension" verbose = true begin
+ ψ = normalize(coherent(50, 5.0) + coherent(50, -5.0))
+ xvec = yvec = -15.0:0.1:15.0
+ wig = transpose(wigner(ψ, xvec, yvec))
+
+ # Makie unload errors
+ @test_throws ArgumentError plot_wigner(ψ; library = :Makie, xvec = xvec, yvec = yvec)
+ @test_throws ArgumentError plot_fock_distribution(ψ; library = :Makie)
+ @test_throws ArgumentError plot_bloch(ψ; library = :Makie)
+
+ using Makie
+
+ fig, ax, hm =
+ plot_wigner(ψ; library = Val(:Makie), xvec = xvec, yvec = yvec, projection = Val(:two_dim), colorbar = true)
+ @test fig isa Figure
+ @test ax isa Axis
+ @test hm isa Heatmap
+ @test all(isapprox.(hm[3].value.x, wig, atol = 1e-6))
+
+ fig, ax, surf =
+ plot_wigner(ψ; library = Val(:Makie), xvec = xvec, yvec = yvec, projection = Val(:three_dim), colorbar = true)
+ @test fig isa Figure
+ @test ax isa Axis3
+ @test surf isa Surface
+ @test all(isapprox.(surf[3].value.x, wig, atol = 1e-6))
+
+ fig = Figure()
+ pos = fig[2, 3]
+ fig1, ax, hm = plot_wigner(
+ ψ;
+ library = Val(:Makie),
+ xvec = xvec,
+ yvec = yvec,
+ projection = Val(:two_dim),
+ colorbar = true,
+ location = pos,
+ )
+ @test fig1 === fig
+ @test fig[2, 3].layout.content[1].content[1, 1].layout.content[1].content === ax
+
+ fig = Figure()
+ pos = fig[2, 3]
+ fig1, ax, surf = plot_wigner(
+ ψ;
+ library = Val(:Makie),
+ xvec = xvec,
+ yvec = yvec,
+ projection = Val(:three_dim),
+ colorbar = true,
+ location = pos,
+ )
+ @test fig1 === fig
+ @test fig[2, 3].layout.content[1].content[1, 1].layout.content[1].content === ax
+
+ fig = Figure()
+ pos = fig[2, 3]
+ fig1, ax = plot_fock_distribution(ψ; library = Val(:Makie), location = pos)
+ @test fig1 === fig
+ @test fig[2, 3].layout.content[1].content[1, 1].layout.content[1].content === ax
+
+ fig = Figure()
+ pos = fig[2, 3]
+ fig1, ax = @test_logs (:warn,) plot_fock_distribution(ψ * 2; library = Val(:Makie), location = pos)
+end
+
+@testset "Makie Bloch sphere" begin
+ ρ = 0.7 * ket2dm(basis(2, 0)) + 0.3 * ket2dm(basis(2, 1))
+ fig, lscene = plot_bloch(ρ)
+ @test fig isa Figure
+ @test lscene isa LScene
+
+ ψ = (basis(2, 0) + basis(2, 1)) / √2
+ fig, lscene = plot_bloch(ψ)
+ @test fig isa Figure
+ @test lscene isa LScene
+
+ ϕ = dag(ψ)
+ fig, lscene = plot_bloch(ϕ)
+ @test fig isa Figure
+ @test lscene isa LScene
+
+ fig = Figure()
+ ax = Axis(fig[1, 1])
+ pos = fig[1, 2]
+ fig1, lscene = plot_bloch(ψ; location = pos)
+ @test fig1 === fig
+
+ b = Bloch()
+ add_points!(b, [0.0, 0.0, 1.0])
+ @test length(b.points) == 1
+ @test b.points[1] ≈ [0.0, 0.0, 1.0]
+
+ pts = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+ add_points!(b, hcat(pts...))
+ @test length(b.points) == 2
+ @test b.points[2] ≈ hcat(pts...)
+ @test_throws ArgumentError add_points!(b, [1 2 3 4])
+ @test_throws ArgumentError add_points!(b, pts; meth = :wrong)
+
+ b = Bloch()
+ add_vectors!(b, [1.0, 1.0, 0.0])
+ @test length(b.vectors) == 1
+ @test isapprox(norm(b.vectors[1]), √2)
+
+ vecs = [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]]
+ add_vectors!(b, vecs)
+ @test length(b.vectors) == 3
+ @test isapprox(norm(b.vectors[2]), 1.0)
+ @test isapprox(norm(b.vectors[3]), 1.0)
+
+ vec_correct = [1, 0, 0]
+ vec_wrong = [1, 0]
+ b = Bloch()
+ add_line!(b, [0, 0, 0], [1, 0, 0])
+ @test length(b.lines) == 1
+ @test b.lines[1][1][3] ≈ [0.0, 0.0]
+ @test_throws ArgumentError add_line!(b, vec_wrong, vec_correct)
+ @test_throws ArgumentError add_line!(b, vec_correct, vec_wrong)
+
+ add_arc!(b, [0, 0, 1], [0, 1, 0], [1, 0, 0])
+ @test length(b.arcs) == 1
+ @test b.arcs[1][3] == [1.0, 0.0, 0.0]
+ @test_throws ArgumentError add_arc!(b, vec_wrong, vec_correct)
+ @test_throws ArgumentError add_arc!(b, vec_correct, vec_wrong)
+ @test_throws ArgumentError add_arc!(b, vec_wrong, vec_correct, vec_correct)
+ @test_throws ArgumentError add_arc!(b, vec_correct, vec_wrong, vec_correct)
+ @test_throws ArgumentError add_arc!(b, vec_correct, vec_correct, vec_wrong)
+
+ b = Bloch()
+ add_points!(b, [0.0, 0.0, 1.0])
+ add_vectors!(b, [1.0, 0.0, 0.0])
+ add_line!(b, [0, 0, 0], [1, 0, 0])
+ add_arc!(b, [0, 1, 0], [0, 0, 1], [1, 0, 0])
+ clear!(b)
+ @test isempty(b.points)
+ @test isempty(b.vectors)
+ @test isempty(b.lines)
+ @test isempty(b.arcs)
+
+ b = Bloch()
+ add_points!(b, hcat([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]))
+ add_vectors!(b, [[1, 1, 0], [0, 1, 1]])
+ add_line!(b, [0, 0, 0], [1, 1, 1])
+ add_arc!(b, [1, 0, 0], [0, 1, 0], [0, 0, 1])
+ try
+ fig, lscene = render(b)
+ @test !isnothing(fig)
+ @test !isnothing(lscene)
+ catch e
+ @test false
+ @info "Render threw unexpected error" exception=e
+ end
+
+ b = Bloch()
+ ψ₁ = normalize(basis(2, 0) + basis(2, 1))
+ ψ₂ = normalize(basis(2, 0) - im * basis(2, 1))
+ add_line!(b, ψ₁, ψ₂; fmt = "r--")
+ try
+ fig, lscene = render(b)
+ @test !isnothing(fig)
+ @test !isnothing(lscene)
+ catch e
+ @test false
+ @info "Render threw unexpected error" exception=e
+ end
+
+ # test `state to Bloch vector` conversion and `add_states!` function
+ b = Bloch()
+ Pauli_Ops = [sigmax(), sigmay(), sigmaz()]
+ ψ = rand_ket(2)
+ ρ = rand_dm(2)
+ states = [ψ, ρ]
+ x = basis(2, 0) + basis(2, 1) # unnormalized Ket
+ ρ1 = 0.3 * rand_dm(2) + 0.4 * rand_dm(2) # unnormalized density operator
+ ρ2 = Qobj(rand(ComplexF64, 2, 2)) # unnormalized and non-Hermitian Operator
+ add_states!(b, states, kind = :vector)
+ add_states!(b, states, kind = :point)
+ @test length(b.vectors) == 2
+ @test length(b.points) == 1
+ @test all(expect(Pauli_Ops, ψ) .≈ (b.vectors[1]))
+ @test all(expect(Pauli_Ops, ρ) .≈ (b.vectors[2]))
+ @test all([b.vectors[j][k] ≈ b.points[1][k, j] for j in (1, 2) for k in (1, 2, 3)])
+ @test_logs (:warn,) (:warn,) (:warn,) (:warn,) add_states!(b, [x, ρ1, ρ2])
+ @test length(b.vectors) == 5
+ @test_throws ArgumentError add_states!(b, states, kind = :wrong)
+
+ th = range(0, 2π; length = 20)
+ xp = cos.(th);
+ yp = sin.(th);
+ zp = zeros(20);
+ pnts = [xp, yp, zp];
+ pnts = Matrix(hcat(xp, yp, zp)');
+ add_points!(b, pnts);
+ vec = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
+ add_vectors!(b, vec);
+ add_line!(b, [1, 0, 0], [0, 1, 0])
+ add_arc!(b, [1, 0, 0], [0, 1, 0], [0, 0, 1])
+ try
+ fig, lscene = render(b)
+ @test !isnothing(fig)
+ @test !isnothing(lscene)
+ catch e
+ @test false
+ @info "Render threw unexpected error" exception=e
+ end
+
+ b = Bloch()
+ ψ₁ = normalize(basis(2, 0) + basis(2, 1))
+ ψ₂ = normalize(basis(2, 0) - im * basis(2, 1))
+ ψ₃ = basis(2, 0)
+ add_line!(b, ψ₁, ψ₂; fmt = "r--")
+ add_arc!(b, ψ₁, ψ₂)
+ add_arc!(b, ψ₂, ψ₃, ψ₁)
+ add_states!(b, [ψ₂, ψ₃], kind = :point, meth = :l)
+ try
+ fig, lscene = render(b)
+ @test !isnothing(fig)
+ @test !isnothing(lscene)
+ catch e
+ @test false
+ @info "Render threw unexpected error" exception=e
+ end
+
+ # if render location is given as lscene, should return the same Figure and LScene
+ b = Bloch()
+ fig1, lscene1 = render(b)
+ add_states!(b, ψ₁)
+ fig2, lscene2 = render(b, location = lscene1)
+ @test fig2 === fig1
+ @test lscene2 === lscene1
+end
diff --git a/test/ext-test/gpu/Project.toml b/test/ext-test/gpu/Project.toml
new file mode 100644
index 000000000..63ad2aa77
--- /dev/null
+++ b/test/ext-test/gpu/Project.toml
@@ -0,0 +1,6 @@
+[deps]
+CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
+QuantumToolbox = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab"
+
+[compat]
+CUDA = "5"
\ No newline at end of file
diff --git a/test/ext-test/gpu/cuda_ext.jl b/test/ext-test/gpu/cuda_ext.jl
new file mode 100644
index 000000000..1f60d5794
--- /dev/null
+++ b/test/ext-test/gpu/cuda_ext.jl
@@ -0,0 +1,266 @@
+@testset "CUDA Extension" verbose = true begin
+ # Test that scalar indexing is disallowed
+ @test_throws ErrorException CUDA.rand(1)[1]
+
+ ψdi = Qobj(Int64[1, 0])
+ ψdf = Qobj(Float64[1, 0])
+ ψdc = Qobj(ComplexF64[1, 0])
+ ψsi = to_sparse(ψdi)
+ ψsf = to_sparse(ψdf)
+ ψsc = to_sparse(ψdc)
+
+ Xdi = Qobj(Int64[0 1; 1 0])
+ Xdf = Qobj(Float64[0 1; 1 0])
+ Xdc = Qobj(ComplexF64[0 1; 1 0])
+ Xsi = to_sparse(Xdi)
+ Xsf = to_sparse(Xdf)
+ Xsc = to_sparse(Xdc)
+
+ @test_throws DomainError cu(ψdi; word_size = 16)
+
+ # type conversion of CUDA dense arrays
+ @test typeof(cu(ψdi; word_size = 64).data) == typeof(CuArray(ψdi).data) <: CuArray{Int64,1}
+ @test typeof(cu(ψdi; word_size = 32).data) == typeof(CuArray{Int32}(ψdi).data) <: CuArray{Int32,1}
+ @test typeof(cu(ψdf; word_size = 64).data) == typeof(CuArray(ψdf).data) <: CuArray{Float64,1}
+ @test typeof(cu(ψdf; word_size = 32).data) == typeof(CuArray{Float32}(ψdf).data) <: CuArray{Float32,1}
+ @test typeof(cu(ψdc; word_size = 64).data) == typeof(CuArray(ψdc).data) <: CuArray{ComplexF64,1}
+ @test typeof(cu(ψdc; word_size = 32).data) == typeof(CuArray{ComplexF32}(ψdc).data) <: CuArray{ComplexF32,1}
+ @test typeof(cu(Xdi; word_size = 64).data) == typeof(CuArray(Xdi).data) <: CuArray{Int64,2}
+ @test typeof(cu(Xdi; word_size = 32).data) == typeof(CuArray{Int32}(Xdi).data) <: CuArray{Int32,2}
+ @test typeof(cu(Xdf; word_size = 64).data) == typeof(CuArray(Xdf).data) <: CuArray{Float64,2}
+ @test typeof(cu(Xdf; word_size = 32).data) == typeof(CuArray{Float32}(Xdf).data) <: CuArray{Float32,2}
+ @test typeof(cu(Xdc; word_size = 64).data) == typeof(CuArray(Xdc).data) <: CuArray{ComplexF64,2}
+ @test typeof(cu(Xdc; word_size = 32).data) == typeof(CuArray{ComplexF32}(Xdc).data) <: CuArray{ComplexF32,2}
+
+ # type conversion of CUDA sparse arrays
+ @test typeof(cu(ψsi; word_size = 64).data) == typeof(CuSparseVector(ψsi).data) == CuSparseVector{Int64,Int32}
+ @test typeof(cu(ψsi; word_size = 32).data) == typeof(CuSparseVector{Int32}(ψsi).data) == CuSparseVector{Int32,Int32}
+ @test typeof(cu(ψsf; word_size = 64).data) == typeof(CuSparseVector(ψsf).data) == CuSparseVector{Float64,Int32}
+ @test typeof(cu(ψsf; word_size = 32).data) ==
+ typeof(CuSparseVector{Float32}(ψsf).data) ==
+ CuSparseVector{Float32,Int32}
+ @test typeof(cu(ψsc; word_size = 64).data) == typeof(CuSparseVector(ψsc).data) == CuSparseVector{ComplexF64,Int32}
+ @test typeof(cu(ψsc; word_size = 32).data) ==
+ typeof(CuSparseVector{ComplexF32}(ψsc).data) ==
+ CuSparseVector{ComplexF32,Int32}
+ @test typeof(cu(Xsi; word_size = 64).data) == typeof(CuSparseMatrixCSC(Xsi).data) == CuSparseMatrixCSC{Int64,Int32}
+ @test typeof(cu(Xsi; word_size = 32).data) ==
+ typeof(CuSparseMatrixCSC{Int32}(Xsi).data) ==
+ CuSparseMatrixCSC{Int32,Int32}
+ @test typeof(cu(Xsf; word_size = 64).data) ==
+ typeof(CuSparseMatrixCSC(Xsf).data) ==
+ CuSparseMatrixCSC{Float64,Int32}
+ @test typeof(cu(Xsf; word_size = 32).data) ==
+ typeof(CuSparseMatrixCSC{Float32}(Xsf).data) ==
+ CuSparseMatrixCSC{Float32,Int32}
+ @test typeof(cu(Xsc; word_size = 64).data) ==
+ typeof(CuSparseMatrixCSC(Xsc).data) ==
+ CuSparseMatrixCSC{ComplexF64,Int32}
+ @test typeof(cu(Xsc; word_size = 32).data) ==
+ typeof(CuSparseMatrixCSC{ComplexF32}(Xsc).data) ==
+ CuSparseMatrixCSC{ComplexF32,Int32}
+ @test typeof(CuSparseMatrixCSR(Xsi).data) == CuSparseMatrixCSR{Int64,Int32}
+ @test typeof(CuSparseMatrixCSR{Int32}(Xsi).data) == CuSparseMatrixCSR{Int32,Int32}
+ @test typeof(CuSparseMatrixCSR(Xsf).data) == CuSparseMatrixCSR{Float64,Int32}
+ @test typeof(CuSparseMatrixCSR{Float32}(Xsf).data) == CuSparseMatrixCSR{Float32,Int32}
+ @test typeof(CuSparseMatrixCSR(Xsc).data) == CuSparseMatrixCSR{ComplexF64,Int32}
+ @test typeof(CuSparseMatrixCSR{ComplexF32}(Xsc).data) == CuSparseMatrixCSR{ComplexF32,Int32}
+
+ # type conversion of CUDA Diagonal arrays
+ @test cu(qeye(10), word_size = Val(32)).data isa Diagonal{ComplexF32,<:CuVector{ComplexF32}}
+ @test cu(qeye(10), word_size = Val(64)).data isa Diagonal{ComplexF64,<:CuVector{ComplexF64}}
+
+ # Sparse To Dense
+ # @test to_dense(cu(ψsi; word_size = 64)).data isa CuVector{Int64} # TODO: Fix this in CUDA.jl
+ @test to_dense(cu(ψsf; word_size = 64)).data isa CuVector{Float64}
+ @test to_dense(cu(ψsc; word_size = 64)).data isa CuVector{ComplexF64}
+ # @test to_dense(cu(Xsi; word_size = 64)).data isa CuMatrix{Int64} # TODO: Fix this in CUDA.jl
+ @test to_dense(cu(Xsf; word_size = 64)).data isa CuMatrix{Float64}
+ @test to_dense(cu(Xsc; word_size = 64)).data isa CuMatrix{ComplexF64}
+
+ # @test to_dense(Int32, cu(ψsf; word_size = 64)).data isa CuVector{Int32} # TODO: Fix this in CUDA.jl
+ # @test to_dense(Float32, cu(ψsf; word_size = 64)).data isa CuVector{Float32} # TODO: Fix this in CUDA.jl
+ # @test to_dense(ComplexF32, cu(ψsf; word_size = 64)).data isa CuVector{ComplexF32} # TODO: Fix this in CUDA.jl
+ # @test to_dense(Int64, cu(Xsf; word_size = 32)).data isa CuMatrix{Int64} # TODO: Fix this in CUDA.jl
+ # @test to_dense(Float64, cu(Xsf; word_size = 32)).data isa CuMatrix{Float64} # TODO: Fix this in CUDA.jl
+ # @test to_dense(ComplexF64, cu(Xsf; word_size = 32)).data isa CuMatrix{ComplexF64} # TODO: Fix this in CUDA.jl
+
+ # brief example in README and documentation
+ N = 20
+ ω64 = 1.0 # Float64
+ ω32 = 1.0f0 # Float32
+ γ64 = 0.1 # Float64
+ γ32 = 0.1f0 # Float32
+ tlist = range(0, 10, 100)
+
+ ## calculate by CPU
+ a_cpu = destroy(N)
+ ψ0_cpu = fock(N, 3)
+ H_cpu = ω64 * a_cpu' * a_cpu
+ sol_cpu = mesolve(H_cpu, ψ0_cpu, tlist, [sqrt(γ64) * a_cpu], e_ops = [a_cpu' * a_cpu], progress_bar = Val(false))
+
+ ## calculate by GPU (with 64-bit)
+ a_gpu64 = cu(destroy(N))
+ ψ0_gpu64 = cu(fock(N, 3))
+ H_gpu64 = ω64 * a_gpu64' * a_gpu64
+ sol_gpu64 = mesolve(
+ H_gpu64,
+ ψ0_gpu64,
+ tlist,
+ [sqrt(γ64) * a_gpu64],
+ e_ops = [a_gpu64' * a_gpu64],
+ progress_bar = Val(false),
+ )
+
+ ## calculate by GPU (with 32-bit)
+ a_gpu32 = cu(destroy(N), word_size = 32)
+ ψ0_gpu32 = cu(fock(N, 3), word_size = 32)
+ H_gpu32 = ω32 * a_gpu32' * a_gpu32
+ sol_gpu32 = mesolve(
+ H_gpu32,
+ ψ0_gpu32,
+ tlist,
+ [sqrt(γ32) * a_gpu32],
+ e_ops = [a_gpu32' * a_gpu32],
+ progress_bar = Val(false),
+ )
+
+ @test all([isapprox(sol_cpu.expect[i], sol_gpu64.expect[i]) for i in 1:length(tlist)])
+ @test all([isapprox(sol_cpu.expect[i], sol_gpu32.expect[i]; atol = 1e-6) for i in 1:length(tlist)])
+end
+
+@testset "CUDA steadystate" begin
+ N = 50
+ Δ = 0.01
+ F = 0.1
+ γ = 0.1
+ nth = 2
+
+ a = destroy(N)
+ H = Δ * a' * a + F * (a + a')
+ c_ops = [sqrt(γ * (nth + 1)) * a, sqrt(γ * nth) * a']
+
+ ρ_ss_cpu = steadystate(H, c_ops)
+
+ H_gpu_csc = cu(H)
+ c_ops_gpu_csc = [cu(c_op) for c_op in c_ops]
+ ρ_ss_gpu_csc = steadystate(H_gpu_csc, c_ops_gpu_csc, solver = SteadyStateLinearSolver())
+
+ H_gpu_csr = CuSparseMatrixCSR(H_gpu_csc)
+ c_ops_gpu_csr = [CuSparseMatrixCSR(c_op) for c_op in c_ops_gpu_csc]
+ ρ_ss_gpu_csr = steadystate(H_gpu_csr, c_ops_gpu_csr, solver = SteadyStateLinearSolver())
+
+ @test ρ_ss_cpu.data ≈ Array(ρ_ss_gpu_csc.data) atol = 1e-8 * length(ρ_ss_cpu)
+ @test ρ_ss_cpu.data ≈ Array(ρ_ss_gpu_csr.data) atol = 1e-8 * length(ρ_ss_cpu)
+end
+
+@testset "CUDA spectrum" begin
+ N = 10
+ a = cu(destroy(N))
+ H = a' * a
+ c_ops = [sqrt(0.1 * (0.01 + 1)) * a, sqrt(0.1 * (0.01)) * a']
+ solver = Lanczos(steadystate_solver = SteadyStateLinearSolver())
+
+ ω_l = range(0, 3, length = 1000)
+ spec = spectrum(H, ω_l, c_ops, a', a; solver = solver)
+
+ spec = collect(spec)
+ spec = spec ./ maximum(spec)
+
+ test_func = maximum(real.(spec)) * (0.1 / 2)^2 ./ ((ω_l .- 1) .^ 2 .+ (0.1 / 2)^2)
+ idxs = test_func .> 0.05
+ @test sum(abs2.(spec[idxs] .- test_func[idxs])) / sum(abs2.(test_func[idxs])) < 0.01
+
+ # TODO: Fix this
+ # @testset "Type Inference spectrum" begin
+ # @inferred spectrum(H, ω_l, c_ops, a', a; solver = solver)
+ # end
+end
+
+@testset "CUDA ptrace" begin
+ g = fock(2, 1)
+ e = fock(2, 0)
+ α = sqrt(0.7)
+ β = sqrt(0.3) * 1im
+ ψ = α * kron(g, e) + β * kron(e, g) |> cu
+
+ ρ1 = ptrace(ψ, 1)
+ ρ2 = ptrace(ψ, 2)
+ @test ρ1.data isa CuArray
+ @test ρ2.data isa CuArray
+ @test Array(ρ1.data) ≈ [0.3 0.0; 0.0 0.7] atol = 1e-10
+ @test Array(ρ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 isa CuArray
+ @test ρ2.data isa CuArray
+ @test Array(ρ1.data) ≈ [0.3 0.0; 0.0 0.7] atol = 1e-10
+ @test Array(ρ2.data) ≈ [0.7 0.0; 0.0 0.3] atol = 1e-10
+
+ ρ = ket2dm(ψ)
+ ρ1 = ptrace(ρ, 1)
+ ρ2 = ptrace(ρ, 2)
+ @test ρ.data isa CuArray
+ @test ρ1.data isa CuArray
+ @test ρ2.data isa CuArray
+ @test Array(ρ1.data) ≈ [0.3 0.0; 0.0 0.7] atol = 1e-10
+ @test Array(ρ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) |> cu
+ ρ1_ptr = ptrace(ρ, 1)
+ ρ2_ptr = ptrace(ρ, 2)
+ @test ρ1_ptr.data isa CuArray
+ @test ρ2_ptr.data isa CuArray
+ @test ρ1.data ≈ Array(ρ1_ptr.data) atol = 1e-10
+ @test ρ2.data ≈ Array(ρ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...) |> cu
+ ρtotal = tensor(ρlist...) |> cu
+ 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) ≈ cu(tensor([ket2dm(ψlist[i]) for i in sel]...))
+ @test ptrace(ρtotal, sel) ≈ cu(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
+
+ @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
diff --git a/test/low_rank_dynamics.jl b/test/low_rank_dynamics.jl
deleted file mode 100644
index 99dd33050..000000000
--- a/test/low_rank_dynamics.jl
+++ /dev/null
@@ -1,80 +0,0 @@
-@testset "Low Rank Dynamics" begin
- # Define lattice
- Nx, Ny = 2, 3
- latt = Lattice(Nx = Nx, Ny = Ny)
- N_cut = 2
- N_modes = latt.N
- N = N_cut^N_modes
- M = Nx * Ny + 1
-
- # Define initial state
- ϕ = 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
- i += 1
- i <= M && (ϕ[i] = mb(sp, j, latt) * ϕ[1])
- end
- for k in 1:N_modes-1
- for l in k+1:N_modes
- i += 1
- i <= M && (ϕ[i] = mb(sp, k, latt) * mb(sp, l, latt) * ϕ[1])
- end
- end
- for i in i+1:M
- ϕ[i] = Qobj(rand(ComplexF64, size(ϕ[1])[1]), dims = ϕ[1].dims)
- normalize!(ϕ[i])
- end
- z = hcat(broadcast(x -> x.data, ϕ)...)
- B = Matrix(Diagonal([1 + 0im; zeros(M - 1)]))
- S = z' * z
- B = B / tr(S * B)
- ρ = Qobj(z * B * z', dims = ntuple(i -> 1, Val(N_modes)) .* N_cut)
-
- # Define Hamiltonian and collapse operators
- Jx = 0.9
- Jy = 1.02
- Jz = 1.0
- hx = 0.0
- γ = 1
- Sz = sum([mb(sz, i, latt) for i in 1:latt.N])
- tl = LinRange(0, 10, 100)
-
- H, c_ops = TFIM(Jx, Jy, Jz, hx, γ, latt; bc = pbc, order = 1)
- e_ops = (Sz,)
-
- # Full solution
- mesol = mesolve(H, ρ, tl, c_ops; e_ops = [e_ops...], progress_bar = Val(false))
- A = Matrix(mesol.states[end].data)
- λ = eigvals(Hermitian(A))
- Strue = -sum(λ .* log2.(λ))
-
- # Low rank solution
- function f_entropy(p, z, B)
- C = p.A0
- σ = p.Bi
- mul!(C, z, sqrt(B))
- mul!(σ, C', C)
- λ = eigvals(Hermitian(σ))
- λ = λ[λ.>1e-10]
- return -sum(λ .* log2.(λ))
- end
-
- opt = LRMesolveOptions(
- err_max = 1e-3,
- p0 = 0.0,
- atol_inv = 1e-6,
- adj_condition = "variational",
- Δt = 0.2,
- progress = false,
- )
- lrsol = lr_mesolve(H, z, B, tl, c_ops; e_ops = e_ops, f_ops = (f_entropy,), opt = opt)
-
- # Test
- m_me = real(mesol.expect[1, :])
- m_lr = real(lrsol.expvals[1, :])
- @test all(abs.((m_me .- m_lr) ./ m_me) .< 0.1)
-
- S_lr = real(lrsol.funvals[1, end])
- @test abs((S_lr - Strue) / Strue) < 0.5
-end
diff --git a/test/negativity_and_partial_transpose.jl b/test/negativity_and_partial_transpose.jl
deleted file mode 100644
index ba10db898..000000000
--- a/test/negativity_and_partial_transpose.jl
+++ /dev/null
@@ -1,44 +0,0 @@
-@testset "Negativity and Partial Transpose" verbose = true begin
- @testset "negativity" begin
- rho = (1 / 40) * Qobj(
- [
- 15 1 1 15
- 1 5 -3 1
- 1 -3 5 1
- 15 1 1 15
- ];
- 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_sparse = dense_to_sparse(A_dense)
- PT = (true, false)
- for s1 in PT
- for s2 in PT
- for s3 in PT
- mask = [s1, s2, s3]
- @test partial_transpose(A_dense, mask) == partial_transpose(A_sparse, mask)
- end
- 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/runtests.jl b/test/runtests.jl
index ea61379d6..96c52f274 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,45 +1,83 @@
using Test
+using TestItemRunner
using Pkg
-using QuantumToolbox
-using QuantumToolbox: position, momentum
+
+const GROUP_LIST = String["All", "Core", "Code-Quality", "AutoDiff_Ext", "Makie_Ext", "CUDA_Ext"]
const GROUP = get(ENV, "GROUP", "All")
+(GROUP in GROUP_LIST) || throw(ArgumentError("Unknown GROUP = $GROUP"))
-const testdir = dirname(@__FILE__)
+# Core tests
+if (GROUP == "All") || (GROUP == "Core")
+ import QuantumToolbox
+
+ QuantumToolbox.about()
+
+ println("\nStart running Core tests...\n")
+ @run_package_tests verbose=true
+end
+
+########################################################################
+# Use traditional Test.jl instead of TestItemRunner.jl for other tests #
+########################################################################
-# Put core tests in alphabetical order
-core_tests = [
- "correlations_and_spectrum.jl",
- "dynamical_fock_dimension_mesolve.jl",
- "dynamical-shifted-fock.jl",
- "eigenvalues_and_operators.jl",
- "entanglement.jl",
- "generalized_master_equation.jl",
- "low_rank_dynamics.jl",
- "negativity_and_partial_transpose.jl",
- "permutation.jl",
- "progress_bar.jl",
- "quantum_objects.jl",
- "states_and_operators.jl",
- "steady_state.jl",
- "time_evolution.jl",
- "wigner.jl",
-]
+const testdir = dirname(@__FILE__)
if (GROUP == "All") || (GROUP == "Code-Quality")
- Pkg.add(["Aqua", "JET"])
- include(joinpath(testdir, "code_quality.jl"))
+ Pkg.activate("core-test/code-quality")
+ Pkg.develop(PackageSpec(path = dirname(@__DIR__)))
+ Pkg.update()
+
+ using QuantumToolbox
+ using Aqua, JET
+
+ (GROUP == "Code-Quality") && QuantumToolbox.about() # print version info. for code quality CI in GitHub
+
+ include(joinpath(testdir, "core-test", "code-quality", "code_quality.jl"))
end
-if (GROUP == "All") || (GROUP == "Core")
+if (GROUP == "AutoDiff_Ext")
+ Pkg.activate("ext-test/cpu/autodiff")
+ Pkg.develop(PackageSpec(path = dirname(@__DIR__)))
+ Pkg.update()
+
+ using QuantumToolbox
+ using ForwardDiff
+ using Zygote
+ using Enzyme
+ using SciMLSensitivity
+
+ QuantumToolbox.about()
+
+ include(joinpath(testdir, "ext-test", "cpu", "autodiff", "autodiff.jl"))
+end
+
+if (GROUP == "Makie_Ext")
+ Pkg.activate("ext-test/cpu/makie")
+ Pkg.develop(PackageSpec(path = dirname(@__DIR__)))
+ Pkg.update()
+
+ using QuantumToolbox
QuantumToolbox.about()
- for test in core_tests
- include(joinpath(testdir, test))
- end
+ # CarioMakie is imported in the following script
+ include(joinpath(testdir, "ext-test", "cpu", "makie", "makie_ext.jl"))
end
-if (GROUP == "CUDA_Ext")# || (GROUP == "All")
- Pkg.add("CUDA")
- include(joinpath(testdir, "cuda_ext.jl"))
+if (GROUP == "CUDA_Ext")
+ Pkg.activate("ext-test/gpu")
+ Pkg.develop(PackageSpec(path = dirname(@__DIR__)))
+ Pkg.update()
+
+ using QuantumToolbox
+ import LinearAlgebra: Diagonal
+ import StaticArraysCore: SVector
+ using CUDA
+ using CUDA.CUSPARSE
+ # CUDA.allowscalar(false) # This is already set in the extension script
+
+ QuantumToolbox.about()
+ CUDA.versioninfo()
+
+ include(joinpath(testdir, "ext-test", "gpu", "cuda_ext.jl"))
end
diff --git a/test/time_evolution.jl b/test/time_evolution.jl
deleted file mode 100644
index b4d097dea..000000000
--- a/test/time_evolution.jl
+++ /dev/null
@@ -1,153 +0,0 @@
-@testset "Time Evolution and Partial Trace" verbose = true begin
- @testset "sesolve" begin
- N = 10
- a_d = kron(create(N), qeye(2))
- a = a_d'
- sx = kron(qeye(N), sigmax())
- sy = tensor(qeye(N), sigmay())
- sz = qeye(N) ⊗ sigmaz()
- η = 0.01
- H = a_d * a + 0.5 * sz - 1im * η * (a - a_d) * sx
- psi0 = kron(fock(N, 0), fock(2, 0))
- t_l = LinRange(0, 1000, 1000)
- e_ops = [a_d * a]
- sol = sesolve(H, psi0, t_l, e_ops = e_ops, progress_bar = Val(false))
- sol2 = sesolve(H, psi0, t_l, progress_bar = Val(false))
- 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 length(sol.states) == 1
- @test size(sol.expect) == (length(e_ops), length(t_l))
- @test length(sol2.states) == length(t_l)
- @test size(sol2.expect) == (0, length(t_l))
- @test length(sol3.states) == length(t_l)
- @test size(sol3.expect) == (length(e_ops), length(t_l))
- @test sol_string ==
- "Solution of time evolution\n" *
- "(return code: $(sol.retcode))\n" *
- "--------------------------\n" *
- "num_states = $(length(sol.states))\n" *
- "num_expect = $(size(sol.expect, 1))\n" *
- "ODE alg.: $(sol.alg)\n" *
- "abstol = $(sol.abstol)\n" *
- "reltol = $(sol.reltol)\n"
-
- @testset "Type Inference sesolve" begin
- @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
- end
-
- @testset "mesolve and mcsolve" begin
- N = 10
- a = destroy(N)
- a_d = a'
- H = a_d * a
- c_ops = [sqrt(0.1) * a]
- e_ops = [a_d * a]
- psi0 = basis(N, 3)
- t_l = LinRange(0, 100, 1000)
- sol_me = mesolve(H, psi0, t_l, c_ops, e_ops = e_ops, progress_bar = Val(false))
- 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_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)
- @test sum(abs.(sol_mc.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)
- @test size(sol_me2.expect) == (0, length(t_l))
- @test length(sol_me3.states) == length(t_l)
- @test size(sol_me3.expect) == (length(e_ops), length(t_l))
- @test sol_me_string ==
- "Solution of time evolution\n" *
- "(return code: $(sol_me.retcode))\n" *
- "--------------------------\n" *
- "num_states = $(length(sol_me.states))\n" *
- "num_expect = $(size(sol_me.expect, 1))\n" *
- "ODE alg.: $(sol_me.alg)\n" *
- "abstol = $(sol_me.abstol)\n" *
- "reltol = $(sol_me.reltol)\n"
- @test sol_mc_string ==
- "Solution of quantum trajectories\n" *
- "(converged: $(sol_mc.converged))\n" *
- "--------------------------------\n" *
- "num_trajectories = $(sol_mc.n_traj)\n" *
- "num_states = $(length(sol_mc.states[1]))\n" *
- "num_expect = $(size(sol_mc.expect, 1))\n" *
- "ODE alg.: $(sol_mc.alg)\n" *
- "abstol = $(sol_mc.abstol)\n" *
- "reltol = $(sol_mc.reltol)\n"
-
- @testset "Type Inference mesolve" begin
- @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
-
- @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))
- end
- end
-
- @testset "exceptions" begin
- N = 10
- a = destroy(N)
- H = a' * a
- psi0 = basis(N, 3)
- t_l = LinRange(0, 100, 1000)
- psi_wrong = basis(N - 1, 3)
- @test_throws DimensionMismatch sesolve(H, psi_wrong, t_l)
- @test_throws DimensionMismatch mesolve(H, psi_wrong, t_l)
- @test_throws DimensionMismatch mcsolve(H, psi_wrong, t_l)
- @test_throws ArgumentError sesolve(H, psi0, t_l, save_idxs = [1, 2])
- @test_throws ArgumentError mesolve(H, psi0, t_l, save_idxs = [1, 2])
- @test_throws ArgumentError mcsolve(H, psi0, t_l, save_idxs = [1, 2])
- end
-
- @testset "example" begin
- sp1 = kron(sigmap(), qeye(2))
- sm1 = sp1'
- sx1 = sm1 + sp1
- sy1 = 1im * (sm1 - sp1)
- sz1 = sp1 * sm1 - sm1 * sp1
- sp2 = kron(qeye(2), sigmap())
- sm2 = sp2'
- sx2 = sm2 + sp2
- sy2 = 1im * (sm2 - sp2)
- sz2 = sp2 * sm2 - sm2 * sp2
- ωq1, ωq2 = 1, 1
- γ1, γ2 = 0.05, 0.1
- H = 0.5 * ωq1 * sz1 + 0.5 * ωq2 * sz2
- c_ops = [sqrt(γ1) * sm1, sqrt(γ2) * sm2]
- psi0_1 = normalize(fock(2, 0) + fock(2, 1))
- psi0_2 = normalize(fock(2, 0) + fock(2, 1))
- psi0 = kron(psi0_1, psi0_2)
- t_l = LinRange(0, 20 / γ1, 1000)
- sol_me = mesolve(H, psi0, t_l, c_ops, e_ops = [sp1 * sm1, sp2 * sm2], progress_bar = false) # Here we don't put Val(false) because we want to test the support for Bool type
- sol_mc = mcsolve(H, psi0, t_l, c_ops, n_traj = 500, e_ops = [sp1 * sm1, sp2 * sm2], progress_bar = Val(false))
- @test sum(abs.(sol_mc.expect[1:2, :] .- sol_me.expect[1:2, :])) / length(t_l) < 0.1
- @test expect(sp1 * sm1, sol_me.states[end]) ≈ expect(sigmap() * sigmam(), ptrace(sol_me.states[end], 1))
- end
-end