wavepacket 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. wavepacket-0.3.0/.github/workflows/ci_testing.yml +30 -0
  2. wavepacket-0.3.0/.gitignore +67 -0
  3. wavepacket-0.3.0/.readthedocs.yaml +18 -0
  4. wavepacket-0.3.0/CHANGELOG.rst +70 -0
  5. {wavepacket-0.1.0 → wavepacket-0.3.0}/LICENSE +1 -1
  6. {wavepacket-0.1.0 → wavepacket-0.3.0}/PKG-INFO +11 -13
  7. wavepacket-0.3.0/doc/Makefile +20 -0
  8. wavepacket-0.3.0/doc/advanced/pendular_states.md +172 -0
  9. wavepacket-0.3.0/doc/advanced/polynomial_solvers.md +233 -0
  10. wavepacket-0.3.0/doc/advanced/thermal_states.md +449 -0
  11. wavepacket-0.3.0/doc/architecture.rst +67 -0
  12. wavepacket-0.3.0/doc/conf.py +65 -0
  13. wavepacket-0.3.0/doc/index.rst +77 -0
  14. wavepacket-0.3.0/doc/license.rst +17 -0
  15. wavepacket-0.3.0/doc/license.txt +121 -0
  16. wavepacket-0.3.0/doc/make.bat +35 -0
  17. wavepacket-0.3.0/doc/representations.rst +130 -0
  18. wavepacket-0.3.0/doc/requirements.txt +248 -0
  19. wavepacket-0.3.0/doc/tutorials/chebychev_solvers.md +218 -0
  20. wavepacket-0.3.0/doc/tutorials/eigenstates.md +85 -0
  21. wavepacket-0.3.0/doc/tutorials/plotting.md +198 -0
  22. wavepacket-0.3.0/doc/tutorials/relaxation.md +227 -0
  23. wavepacket-0.3.0/doc/tutorials/schroedinger_cat.md +169 -0
  24. wavepacket-0.3.0/pyproject.toml +71 -0
  25. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/__init__.py +16 -12
  26. wavepacket-0.3.0/src/wavepacket/builder/__init__.py +9 -0
  27. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/builder/density.py +30 -4
  28. wavepacket-0.3.0/src/wavepacket/builder/wave_function.py +103 -0
  29. wavepacket-0.3.0/src/wavepacket/expression/__init__.py +10 -0
  30. wavepacket-0.3.0/src/wavepacket/expression/expressionbase.py +105 -0
  31. wavepacket-0.3.0/src/wavepacket/expression/liouvillian.py +84 -0
  32. wavepacket-0.3.0/src/wavepacket/expression/schroedingerequation.py +39 -0
  33. wavepacket-0.3.0/src/wavepacket/functions.py +90 -0
  34. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/generators.py +50 -8
  35. wavepacket-0.3.0/src/wavepacket/grid/__init__.py +13 -0
  36. wavepacket-0.3.0/src/wavepacket/grid/_utils.py +27 -0
  37. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/grid/dofbase.py +19 -36
  38. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/grid/grid.py +28 -51
  39. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/grid/planewavedof.py +13 -19
  40. wavepacket-0.3.0/src/wavepacket/grid/spherical_harmonics_dof.py +172 -0
  41. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/grid/state.py +35 -12
  42. wavepacket-0.3.0/src/wavepacket/grid/state_utilities.py +279 -0
  43. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/logging.py +6 -6
  44. wavepacket-0.3.0/src/wavepacket/operator/__init__.py +14 -0
  45. wavepacket-0.3.0/src/wavepacket/operator/_clipping.py +36 -0
  46. wavepacket-0.3.0/src/wavepacket/operator/fbroperators.py +225 -0
  47. wavepacket-0.3.0/src/wavepacket/operator/misc_operators.py +113 -0
  48. wavepacket-0.1.0/src/wavepacket/operator/operatorutils.py → wavepacket-0.3.0/src/wavepacket/operator/operator_utils.py +11 -6
  49. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/operator/operatorbase.py +102 -29
  50. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/operator/potentials.py +24 -5
  51. wavepacket-0.3.0/src/wavepacket/operator/time_dependent_operators.py +70 -0
  52. wavepacket-0.3.0/src/wavepacket/plot/__init__.py +9 -0
  53. wavepacket-0.3.0/src/wavepacket/plot/plot_1d.py +221 -0
  54. wavepacket-0.3.0/src/wavepacket/py.typed +0 -0
  55. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/solver/__init__.py +4 -1
  56. wavepacket-0.3.0/src/wavepacket/solver/chebychev.py +182 -0
  57. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/solver/odesolver.py +7 -9
  58. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/solver/solverbase.py +19 -25
  59. wavepacket-0.3.0/src/wavepacket/solver/tise.py +66 -0
  60. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/testing/dummydof.py +3 -1
  61. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/testing/dummyoperator.py +4 -5
  62. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/testing/random.py +1 -1
  63. wavepacket-0.3.0/src/wavepacket/typing/__init__.py +7 -0
  64. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/typing/data_types.py +7 -1
  65. wavepacket-0.3.0/test/builder/test_density.py +78 -0
  66. wavepacket-0.3.0/test/builder/test_product_wave_function.py +42 -0
  67. wavepacket-0.3.0/test/builder/test_wave_function.py +39 -0
  68. wavepacket-0.3.0/test/conftest.py +14 -0
  69. wavepacket-0.3.0/test/expression/test_expression_sum.py +29 -0
  70. wavepacket-0.3.0/test/expression/test_liouvillian.py +71 -0
  71. wavepacket-0.3.0/test/expression/test_schroedingerequation.py +32 -0
  72. wavepacket-0.3.0/test/grid/test_dofbase.py +52 -0
  73. wavepacket-0.3.0/test/grid/test_grid.py +105 -0
  74. wavepacket-0.3.0/test/grid/test_planewavedof_basics.py +60 -0
  75. wavepacket-0.3.0/test/grid/test_planewavedof_transformations.py +125 -0
  76. wavepacket-0.3.0/test/grid/test_sphericalharmonicsdof_basics.py +50 -0
  77. wavepacket-0.3.0/test/grid/test_sphericalharmonicsdof_transformations.py +110 -0
  78. wavepacket-0.3.0/test/grid/test_state.py +103 -0
  79. wavepacket-0.3.0/test/grid/test_state_utilities.py +192 -0
  80. wavepacket-0.3.0/test/operator/test_clipping.py +53 -0
  81. wavepacket-0.3.0/test/operator/test_fbroperator.py +97 -0
  82. wavepacket-0.3.0/test/operator/test_misc_operators.py +112 -0
  83. wavepacket-0.3.0/test/operator/test_operator_arithmetic.py +124 -0
  84. wavepacket-0.3.0/test/operator/test_operatorbase.py +41 -0
  85. wavepacket-0.3.0/test/operator/test_operatorutils.py +53 -0
  86. wavepacket-0.3.0/test/operator/test_planewavefbroperator.py +94 -0
  87. wavepacket-0.3.0/test/operator/test_potential1d.py +70 -0
  88. wavepacket-0.3.0/test/operator/test_time_dependent_operators.py +35 -0
  89. wavepacket-0.3.0/test/solver/test_chebychev.py +125 -0
  90. wavepacket-0.3.0/test/solver/test_odesolver.py +46 -0
  91. wavepacket-0.3.0/test/solver/test_solverbase.py +57 -0
  92. wavepacket-0.3.0/test/solver/test_tise.py +63 -0
  93. wavepacket-0.3.0/test/test_base_exports.py +5 -0
  94. wavepacket-0.3.0/test/test_functions.py +56 -0
  95. wavepacket-0.3.0/test/test_generators.py +71 -0
  96. wavepacket-0.3.0/test/test_logging.py +8 -0
  97. wavepacket-0.1.0/pyproject.toml +0 -41
  98. wavepacket-0.1.0/src/wavepacket/builder/__init__.py +0 -8
  99. wavepacket-0.1.0/src/wavepacket/builder/wave_function.py +0 -63
  100. wavepacket-0.1.0/src/wavepacket/expression/__init__.py +0 -10
  101. wavepacket-0.1.0/src/wavepacket/expression/expressionbase.py +0 -44
  102. wavepacket-0.1.0/src/wavepacket/expression/liouvillian.py +0 -64
  103. wavepacket-0.1.0/src/wavepacket/expression/schroedingerequation.py +0 -65
  104. wavepacket-0.1.0/src/wavepacket/grid/__init__.py +0 -12
  105. wavepacket-0.1.0/src/wavepacket/grid/state_utilities.py +0 -89
  106. wavepacket-0.1.0/src/wavepacket/operator/__init__.py +0 -12
  107. wavepacket-0.1.0/src/wavepacket/operator/fbroperators.py +0 -94
  108. wavepacket-0.1.0/src/wavepacket/typing/__init__.py +0 -7
  109. {wavepacket-0.1.0 → wavepacket-0.3.0}/README.rst +0 -0
  110. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/exceptions.py +0 -0
  111. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/testing/__init__.py +0 -0
  112. {wavepacket-0.1.0 → wavepacket-0.3.0}/src/wavepacket/testing/assertions.py +0 -0
@@ -0,0 +1,30 @@
1
+ name: Wavepacket CI
2
+ run-name: Testing ${{ github.ref_name }} with ref ${{ github.ref }}
3
+
4
+ on:
5
+ push
6
+
7
+ jobs:
8
+ Run-all-tests:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v6
13
+ id: cp311
14
+ with:
15
+ python-version: '3.11'
16
+ - uses: actions/setup-python@v6
17
+ id: cp312
18
+ with:
19
+ python-version: '3.12'
20
+ - uses: actions/setup-python@v6
21
+ id: cp313
22
+ with:
23
+ python-version: '3.13'
24
+ - uses: actions/setup-python@v6
25
+ id: cp314
26
+ with:
27
+ python-version: '3.14'
28
+ - run: ${{ steps.cp313.outputs.python-path }} -m venv ~/venv
29
+ - run: ~/venv/bin/python -m pip install tox
30
+ - run: cd $GITHUB_WORKSPACE && ~/venv/bin/tox
@@ -0,0 +1,67 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Installer logs
27
+ pip-log.txt
28
+ pip-delete-this-directory.txt
29
+
30
+ # Unit test / coverage reports
31
+ htmlcov/
32
+ .tox/
33
+ .nox/
34
+ .coverage
35
+ .coverage.*
36
+ .cache
37
+ nosetests.xml
38
+ coverage.xml
39
+ *.cover
40
+ *.py,cover
41
+ .hypothesis/
42
+ .pytest_cache/
43
+ cover/
44
+
45
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
46
+ __pypackages__/
47
+
48
+ # mkdocs documentation
49
+ /site
50
+
51
+ # mypy
52
+ .mypy_cache/
53
+ .dmypy.json
54
+ dmypy.json
55
+
56
+ # Pyre type checker
57
+ .pyre/
58
+
59
+ # pytype static type analyzer
60
+ .pytype/
61
+
62
+ # PyCharm
63
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
64
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
65
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
66
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
67
+ .idea/
@@ -0,0 +1,18 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-22.04
5
+ tools:
6
+ python: "3.13"
7
+ apt_packages:
8
+ - dvipng
9
+ - texlive-binaries
10
+ formats:
11
+ - htmlzip
12
+ sphinx:
13
+ configuration: doc/conf.py
14
+ python:
15
+ install:
16
+ - requirements: doc/requirements.txt
17
+ - method: pip
18
+ path: .
@@ -0,0 +1,70 @@
1
+ =========
2
+ ChangeLog
3
+ =========
4
+
5
+ 0.3
6
+ ---
7
+
8
+ - (#32) added ChebychevSolvers
9
+ - added a ChebychevSolver for real-time propagation
10
+ - added RelaxationSolver for imaginary-time propagation
11
+ - added function to normalize a state
12
+ - Added various documentation on Chebychev solver use, relaxation,
13
+ and polynomial solver theory
14
+
15
+ - (#24) added ExpressionSum and OneSidedLiouvillian
16
+ for more complex expressions
17
+
18
+ - (#31) added diagonalize() function for operator eigenstates and -energies
19
+
20
+ - (#11) operator truncation and systematic handling of operator time-dependence
21
+
22
+ - (#12) Added some documentation on the construction of thermal states
23
+
24
+ - (#12) Changed the implementation of builder.random_wave_function();
25
+ it now constructs random numbers exp(i * phi) because these are more useful.
26
+
27
+ - (#13) Tutorials and demos also can be downloaded as Jupyter notebooks
28
+ and mostly render fine as notebooks.
29
+
30
+ - significantly improved type information, simplified attributes,
31
+ implemented some annotation best practices
32
+
33
+
34
+ 0.2
35
+ ---
36
+
37
+ - (#18) added spherical harmonics expansion:
38
+ degree of freedom, operator, generator
39
+
40
+ - (#19) added time-dependent functions and laser fields as operators,
41
+ including helper functions for sin**2 and rectangular pulses with soft turn-on
42
+
43
+ - (#20) added functionality related to projections
44
+ - projection operator for projecting onto a state or subspace
45
+ - population() function to easily calculate populations of target states
46
+ - Gram-Schmidt orthogonalization and normalization
47
+
48
+ - (#21) added more initial states
49
+ random wave functions, zero densities and wave functions, unit density
50
+ - (#21) added function grid.fbr_density() to calculate density in FBR
51
+
52
+ - (#22) 1D plotting helpers
53
+ - added plotting classes StackedPlot1D and SimplePlot1D
54
+ - added a tutorial on plotting
55
+
56
+ - (#23) Added a notebook with the PendularStates demos converted from the older versions
57
+
58
+ - (#15) added CI build using tox for different Python versions
59
+ Wavepacket now supports every Python >= 3.11
60
+
61
+
62
+ 0.1
63
+ ---
64
+
65
+ Initial release.
66
+
67
+ Scope is the simulation of a harmonic oscillator with an ordinary ODE solver,
68
+ plus a bit of introductory documentation and a demo.
69
+
70
+ That is, infrastructure, not so much content.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Ulf Lorenz
3
+ Copyright (c) 2024-2025 Ulf Lorenz
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,29 +1,27 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavepacket
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: A package for the propagation of quantum-mechanical wave functions.
5
5
  Author: Ulf Lorenz
6
- Requires-Python: >=3.12
6
+ Requires-Python: >=3.11
7
7
  Description-Content-Type: text/x-rst
8
- Classifier: License :: OSI Approved :: MIT License
9
8
  Classifier: Development Status :: 3 - Alpha
10
9
  Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: MacOS :: MacOS X
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Operating System :: POSIX
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
11
18
  Classifier: Topic :: Scientific/Engineering :: Chemistry
12
19
  Classifier: Topic :: Scientific/Engineering :: Physics
13
20
  License-File: LICENSE
14
21
  Requires-Dist: numpy >= 2.1.3
15
22
  Requires-Dist: scipy >= 1.14.1
16
- Requires-Dist: matplotlib >= 3.10 ; extra == "demos"
17
- Requires-Dist: sphinx >= 8.1.3 ; extra == "doc"
18
- Requires-Dist: sphinx-autoapi >= 3.4.0 ; extra == "doc"
19
- Requires-Dist: numpydoc >= 1.8.0 ; extra == "doc"
20
- Requires-Dist: myst-nb >= 1.2 ; extra == "doc"
21
- Requires-Dist: sphinx-rtd-theme >= 3.0.2 ; extra == "doc"
22
- Requires-Dist: pytest >= 8.3 ; extra == "test"
23
+ Requires-Dist: matplotlib >= 3.10
23
24
  Project-URL: Home, https://github.com/ulflor/wavepacket
24
- Provides-Extra: demos
25
- Provides-Extra: doc
26
- Provides-Extra: test
27
25
 
28
26
  Description
29
27
  -----------
@@ -0,0 +1,20 @@
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = .
9
+ BUILDDIR = _build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@@ -0,0 +1,172 @@
1
+ ---
2
+ file_format: mystnb
3
+ kernelspec:
4
+ name: python3
5
+ ---
6
+
7
+ # Pendular states
8
+
9
+ This web page can be downloaded as notebook: {nb-download}`pendular_states.ipynb` (Jupyter)
10
+ or {download}`pendular_states.md` (Markdown)
11
+
12
+ The goal of this demo is the partial reproduction of some results of a paper by Ortigoso et al.[^ortigoso]
13
+ Besides discussing pretty cool physics, it aims to demonstrate
14
+ how to extract non-trivial data with minimal fuss and plot it.
15
+
16
+ ## Alignment of molecules, some theory
17
+
18
+ If a molecule interacts with a non-resonant laser field, the electronic ground state is shifted in energy.
19
+ We can calculate the shift with standard perturbation theory and cavity-dressed states as
20
+
21
+ $$
22
+ \Delta E \propto - \sum_n \frac{|\langle \Phi_0 | \hat{\vec{\mu}} \vec E |\Phi_n \rangle|^2}{E_n - E_i - \omega}
23
+ = - \frac{1}{2} \alpha(\omega) E^2 \cos^2 \theta
24
+ $$
25
+
26
+ in terms of the dipole operator $\mu$, the electric field with strength $E$ and frequency $\omega$,
27
+ and where the summation includes all excited states $\Phi_n$ with energies $E_n$.
28
+ Calculating the energy shift is difficult, but that is not our goal here.
29
+ Instead, we absorb all these calculations in a material-specific dynamic polarizability $\alpha(\omega)$.
30
+ Then only a dependency on the angle $\theta$ between the effective dipole moment and the laser
31
+ polarization axis remains.
32
+
33
+ If we plug this energy shift into the formula of a rigid linear rotor, we arrive at the Hamiltonian
34
+
35
+ $$
36
+ \hat H = \frac{\hat{L}^2}{2I} - \frac{1}{2} \ \alpha E^2 \cos^2\theta.
37
+ $$
38
+
39
+ This Hamiltonian describes a linear rotor (first term) trapped in a cosine-shaped potential that draws the
40
+ rotor towards the laser polarization axis ($\alpha > 0$) or away from it ($\alpha < 0$).
41
+ In the following, we only consider the former case of a positive polarizability.
42
+ As the final step, we introduce three further manipulations:
43
+
44
+ 1. We assume that the electric field changes slowly over time. Effectively, this adds a time-dependent shape function
45
+ to the second term in the Hamiltonian. We will follow the reference and assume a Gaussian shape.
46
+ 2. Because only the product of electric field strength and polarizability matters, we replace it by a
47
+ single parameter.
48
+ 3. To get rid of the moment of inertia, we rescale the time such that we can set $I=1$.
49
+
50
+ With these manipulations, we arrive at the final formulation of a scaled model Hamiltonian
51
+
52
+ $$
53
+ \hat H = \frac{\hat{L}^2}{2} - \frac{\Delta}{2} \ \cos^2\theta \ \mathrm{e}^{- (t - \delta)^2 / \sigma^2}
54
+ $$
55
+
56
+ In the following, we will follow ref.[^ortigoso] further by studying the dynamics of this Hamiltonian
57
+ in different parameter regimes.
58
+
59
+ ## Non-adiabatic alignment
60
+
61
+ The natural timescales of the scaled Hamiltonian are determined by the energy levels of the free rotor.
62
+ For low-lying rotational states, these time scales are somewhere on the order of 0.1 ... 1.
63
+ If the laser pulse is shorter than this timescale, the laser effectively "kicks" the rotor,
64
+ after which it starts to tumble.
65
+
66
+ As alignment measure, we usually employ the expectation value of the squared cosine.
67
+ In the dynamics shown below, we can clearly see the out of equilibrium dynamics after the laser pulse at t = 0.15.
68
+ This plot corresponds to the first graph of figure 1 of ref.[^ortigoso].
69
+ The stronger the laser field the faster the subsequent dynamics of the rotor.
70
+
71
+ Note that at certain points in time, the rotor exhibits alignment recurrence even after the laser field has passed.
72
+ This field-free alignment is used in practice because the molecule is aligned, yet undisturbed by external fields.
73
+
74
+ ```{code-cell}
75
+ import math
76
+ import matplotlib.pyplot as plt
77
+ import numpy as np
78
+
79
+ import wavepacket as wp
80
+
81
+ def calculate_alignment(Delta, sigma, l0=0, m=0):
82
+ delay = 3 * sigma
83
+
84
+ # tiny optimization: smaller grids are faster
85
+ thetaDof = wp.grid.SphericalHarmonicsDof(25+l0, m)
86
+ grid = wp.grid.Grid(thetaDof)
87
+
88
+ psi0 = wp.builder.product_wave_function(grid, wp.SphericalHarmonic(l0, m))
89
+
90
+ kinetic = wp.operator.RotationalKineticEnergy(grid, 0, 0.5)
91
+ cos2 = wp.operator.Potential1D(grid, 0, lambda theta: np.cos(theta)**2)
92
+ laser = wp.operator.TimeDependentOperator(grid,
93
+ lambda t: Delta * math.exp(-(t-delay)**2/sigma**2))
94
+
95
+ hamiltonian = kinetic - 0.5 * cos2 * laser
96
+ equation = wp.expression.SchroedingerEquation(hamiltonian)
97
+ solver = wp.solver.OdeSolver(equation, dt=sigma / 50)
98
+
99
+ results = [(t, wp.operator.expectation_value(cos2, psi))
100
+ for (t, psi) in solver.propagate(psi0, 0, 500)]
101
+ times = np.array([t for (t, _) in results])
102
+ expectation_values = np.array([abs(val) for (_, val) in results])
103
+
104
+ return times, expectation_values
105
+
106
+ times, expectation_values_100 = calculate_alignment(Delta=100, sigma=0.05)
107
+ _, expectation_values_400 = calculate_alignment(Delta=400, sigma=0.05)
108
+ _, expectation_values_900 = calculate_alignment(Delta=900, sigma=0.05)
109
+ plt.plot(times, expectation_values_100, '-k',
110
+ times, expectation_values_400, '--k',
111
+ times, expectation_values_900, ':k');
112
+ ```
113
+
114
+ Note that the actual work is encapsulated in a function with only a few parameters.
115
+ This was a deliberate choice to make the code reusable over this whole demo,
116
+ hence the parameters "l0" (initial angular momentum) and "m" (magnetic quantum number), which will only be
117
+ used further below.
118
+ When changing the laser field parameters, you would otherwise have to retype quite some boilerplate
119
+ code to eventually recreate the solver.
120
+ Encapsulation saves us some noise here, plus it guarantees a homogenous setup.
121
+
122
+ ## Adiabatic alignment
123
+
124
+ If the laser pulse is much longer than the relevant rotational time scales, we are in the adiabatic limit.
125
+ The rotor aligns with the laser pulse when the latter is turned on
126
+ and largely regresses to the field-free state as the laser is turned off.
127
+ This plot corresponds to the third graph of figure 1 of ref[^ortigoso].
128
+
129
+ ```{code-cell}
130
+ times, expectation_values_100 = calculate_alignment(Delta=100, sigma=5)
131
+ _, expectation_values_400 = calculate_alignment(Delta=400, sigma=5)
132
+ _, expectation_values_900 = calculate_alignment(Delta=900, sigma=5)
133
+ plt.plot(times, expectation_values_100, '-k',
134
+ times, expectation_values_400, '--k',
135
+ times, expectation_values_900, ':k');
136
+ ```
137
+
138
+ Stronger laser fields correspond to a higher degree of alignment, although this converges for large field strengths.
139
+ The alignment is much stronger than in the non-adiabatic case, but at the cost of an external alignment field
140
+ that may perturb molecular dynamics.
141
+
142
+ ## Alignment of an excited rotational state
143
+
144
+ Finally, we can also study the alignment of rotationally excited states.
145
+ A new feature in this case is that the magnetic quantum number "m" no longer needs to be zero.
146
+ Let us reproduce figure 3 of ref.[^ortigoso].
147
+
148
+ ```{code-cell}
149
+ data = [calculate_alignment(Delta=400, sigma=0.05, l0=5, m=m) for m in range(6)]
150
+
151
+ times = data[0][0]
152
+ results = [vals for (t, vals) in data]
153
+
154
+ # factor of 2 because positive and negative m yield the same result
155
+ average = results[0]
156
+ for m in range(1,6):
157
+ average = average + 2 * results[m]
158
+ average /= 11
159
+
160
+ plt.plot(times, average, '-k',
161
+ times, results[0], '-.k',
162
+ times, results[2], '--k',
163
+ times, results[4], ':k');
164
+ ```
165
+
166
+ The plot shows the average over all magnetic quantum numbers (solid line), and the individual results for
167
+ m = 0, 2, 4 (dot-dashed, dashed, dotted line).
168
+ While an increasing magnetic quantum number results in less field-free alignment, the total alignment is
169
+ similar for all even values of m.
170
+ The averaged alignment, however, is significantly damped, and smaller due to the lower alignment of odd values of m.
171
+
172
+ [^ortigoso]: Ortigoso et al. <https://doi.org/10.1063/1.478241>
@@ -0,0 +1,233 @@
1
+ ---
2
+ file_format: mystnb
3
+ kernelspec:
4
+ name: python3
5
+ ---
6
+
7
+ # Background theory: Polynomial solvers
8
+
9
+ This web page can be downloaded as notebook: {nb-download}`polynomial_solvers.ipynb` (Jupyter)
10
+ or {download}`polynomial_solvers.md` (Markdown)
11
+
12
+ The usage of the most common Chebychev solver is documented in {doc}`/tutorials/chebychev_solvers`,
13
+ see there on the practical aspects.
14
+ Here, we want to give some more theoretical details on the subject for those inclined to dig deeper.
15
+ In particular, we shall derive the formulas in a little more depth, discuss the magic values for the alpha
16
+ number, and compare the performance of Chebychev solver to that of ODE solvers.
17
+
18
+ We will restrict the discussion to closed systems with real eigenvalues.
19
+ The use of Chebychev polynomials was pioneered by Tal-Ezer and Kosloff [^ChebychevReal].
20
+ It is possible to extend the treatment to purely imaginary eigenvalues, this is discussed in
21
+ {doc}`/tutorials/relaxation`, and the original work [^ChebychevImag].
22
+ An extension of the Chebychev method to arbitrary complex-valued systems (open systems, absorbing boundary conditions)
23
+ was introduced by Huisinga et al. [^faber], but we omit this discussion for simplicity.
24
+
25
+ ## Basic theory
26
+
27
+ For a time-independent, closed system, the time evolution operator is defined by
28
+
29
+ $$
30
+ \psi(t+\Delta t) = \hat U(\Delta t) \psi(t) = \mathrm{e}^{-\imath \hat H \Delta t} \psi(t).
31
+ $$
32
+
33
+ ```{note}
34
+ This and the following discussion also applies to density operators. Replace all operators by
35
+ corresponding Liouvillians (operators in dual space) and the wave function by the density operator (state in dual
36
+ space) and all arguments can be trivially translated.
37
+ ```
38
+
39
+ The basic idea of polynomial solvers is to expand this exponential in a convenient series of classical polynomials.
40
+ Complex Chebychev polynomials are used because of their superior convergence properties.
41
+ Our first problem is that this polynomial series converges only in the interval [-i, i].
42
+ Translated appropriately, this means that the Hamiltonian must have all eigenvalues in the interval [-1, 1].
43
+ To get there, we first rescale the Hamiltonian,
44
+
45
+ $$
46
+ \mathrm{e}^{-\imath \hat H \Delta t} = \mathrm{e}^{-\imath (E_{\mathrm{min}} + \Delta E / 2) \Delta t}
47
+ \mathrm{e}^{-\imath \alpha \hat H_\mathrm{norm}}
48
+ $$
49
+
50
+ where
51
+
52
+ \begin{eqnarray*}
53
+ \alpha =& \frac{\Delta E \ \Delta t}{2}\\
54
+ \hat H_\mathrm{norm} =& \frac{2}{\Delta E} (\hat H - E_\mathrm{min} - \frac{\Delta E}{2})
55
+ .
56
+ \end{eqnarray*}
57
+
58
+ Here, $E_\mathrm{min}, \Delta E$ denote the Hamiltonian's smallest eigenvalue and the spectral range,
59
+ respectively.
60
+ It can be checked that the normalized Hamiltonian has all eigenvalues in the interval [-1, 1].
61
+
62
+ The first exponent is just a constant factor,
63
+ and we only expand the second exponential with the normalized Hamiltonian and "time" $\alpha$ as
64
+
65
+ $$
66
+ \mathrm{e}^{-\imath \hat H_\mathrm{norm} \alpha} = \sum_{n=0}^N a_n(\alpha) \phi_n(-\imath \hat H_\mathrm{norm}),
67
+ $$
68
+
69
+ where up to constants, the coefficients "a" are Bessel functions of the first kind and the functions are the
70
+ complex Chebychev polynomials.
71
+
72
+ ## Convergence
73
+
74
+ The huge advantage of Chebychev polynomials is the availability of an exact upper bound for the propagation error.
75
+ We start with two inequalities, see [^abramowitz], equations 22.14.4 and 9.1.62, which readily generalize to
76
+ functions of operators:
77
+
78
+ \begin{align*}
79
+ |\phi_n(x)| =& |T_n(-\imath x)| &\leq 1 (x \in [-\imath, \imath])
80
+ \qquad \rightarrow \qquad \|\phi_n(-\imath \hat H_\mathrm{norm}) \psi\| \leq \|\psi\|\\
81
+ |J_n(x)| \leq& \frac{|x/2|^n}{n!}
82
+ \end{align*}
83
+
84
+ Now let us assume that we used the first N terms of the expansion and want to know the error when propagating a state.
85
+ We get for a normalized state
86
+
87
+ \begin{align*}
88
+ \| \mathrm{e}^{-\imath \alpha \hat H_\mathrm{norm}} \psi \
89
+ - \ \sum_{n=0}^N a_n(\alpha) \phi_n(-\imath \hat H_\mathrm{norm}) \psi \|
90
+ =& \| \sum_{n=N+1}^\infty a_n(\alpha) \phi_n(-\imath \hat H_\mathrm{norm} \psi)\|\\
91
+ \leq& \sum_{n=N+1}^\infty a_n(\alpha) \|\phi_n(-\imath \hat H_\mathrm{norm} \psi)\|\\
92
+ \leq& \sum_{n=N+1}^\infty a_n(\alpha)\\
93
+ \leq& \sum_{n=N+1}^\infty \frac{(\alpha/2)^n}{n!}
94
+ \end{align*}
95
+
96
+ We do not care about the exact value of the error, just note two properties of the expression:
97
+
98
+ 1. There is an upper bound for the error independent of the initial state. That is, the convergence is continuous,
99
+ there are no individual states for which the series does not converge.
100
+ 2. For large enough N, the right-hand side decreases exponentially with increasing N.
101
+
102
+ In practice, we do not target a specific value of the (upper bound of the) propagation error,
103
+ but simply truncate the series as soon as the Bessel function has reached a certain cutoff.
104
+ This cutoff can still be considered a proxy for the order of magnitude of the error,
105
+ but is not a rigorous quantity.
106
+
107
+ ## Efficiency of the Chebychev solver and good alpha values
108
+
109
+ Now we can finally discuss good alpha values for acceptable efficiencies.
110
+ This discussion is very brief in the original papers.
111
+
112
+ The computational cost is essentially proportional to the order of expansion n;
113
+ the Chebychev polynomials are calculated through a recursion relation, and each recursion requires one expensive
114
+ evaluation of the Hamiltonian / Liouvillian.
115
+ The order of expansion, that is, the cost, is defined by the Bessel functions dropping below a certain threshold.
116
+ The size of the time step, that is, the gain, however, is proportional to the alpha value, i.e., the
117
+ argument of said Bessel function.
118
+
119
+ So we can rephrase the question: At which value of alpha do we get most gain per cost?
120
+ Let us plot the behaviour of the Bessel functions for different values of alpha
121
+
122
+ ```{code-cell}
123
+ import matplotlib.pyplot as plt
124
+ import numpy as np
125
+ import scipy
126
+
127
+ alpha, n = np.meshgrid([20, 40, 60, 80], np.arange(120))
128
+ y = np.abs(scipy.special.jv(n, alpha))
129
+
130
+ fig, ax = plt.subplots()
131
+ ax.set_ylim(1e-12, 1)
132
+ ax.semilogy(n, y);
133
+ ```
134
+
135
+ We can clearly see that the Bessel functions are significant up to $n \approx \alpha$, then they decay rapidly,
136
+ approaching values of 1e-12 within roughly 30 orders.
137
+ No matter our timestep, we always have to include these thirty orders to get a well-converged solution.
138
+ Hence, a good alpha value should definitely not be smaller than about 30.
139
+
140
+ As an example, let us compare alpha values of 20 and 60.
141
+ For the former, we need an order of 50, but our time step is shorter, and we need to step
142
+ three times. Altogether that makes approximately 150 applications of the Hamiltonian.
143
+ For the latter, we need a single time step with an order of 90, hence about 90 applications of
144
+ the Hamiltonian.
145
+ Thus, the latter alpha value requires only about 60% of the computing time of the smaller alpha value.
146
+
147
+ This gain starts to level off when alpha becomes larger than this constant decay.
148
+ Hence, the proposition of using an alpha value of at least 40 to avoid inefficiencies, and there is little point
149
+ of increasing alpha beyond, say, 100.
150
+ Both values are not exact numbers, however, just crude rules of thumb.
151
+
152
+ However, very large orders of expansion may lead to artefacts.
153
+ For example the values of the Bessel functions may become inaccurate, depending on the implementation details.
154
+ Therefore, we would recommend not increasing alpha beyond something like 100, but that is also not a hard value.
155
+ For example, we encountered a Matlab version around 2010
156
+ that would produce infinite solutions for large orders (200 or 300).
157
+
158
+ As a side note, the plot also shows that the precision has remarkably little influence on the efficiency.
159
+ Even if we go for embarrassingly large values of 1e-6, the required polynomial orders are reduced only by about 10,
160
+ which is why we use a fixed value of 1e-12 for the cutoff.
161
+
162
+ ## Comparison between Chebychev and ODE solvers
163
+
164
+ So far we have only *claimed* that the Chebychev solver is much faster than ODE solvers,
165
+ so let us back this up with numbers.
166
+ We choose the truncated harmonic oscillator example from {doc}`/tutorials/chebychev_solvers`.
167
+
168
+ ```{code-cell}
169
+ import math
170
+ import wavepacket as wp
171
+
172
+ grid = wp.grid.Grid(wp.grid.PlaneWaveDof(-10, 10, 128))
173
+ psi0 = wp.builder.product_wave_function(grid, wp.Gaussian(-5, 0, rms=1))
174
+
175
+ kinetic = wp.operator.CartesianKineticEnergy(grid, 0, mass=1, cutoff=35)
176
+ potential = wp.operator.Potential1D(grid, 0, lambda x: 0.5 * x ** 2, cutoff=35)
177
+ equation = wp.expression.SchroedingerEquation(kinetic + potential)
178
+ ```
179
+
180
+ First, we need to measure the speed.
181
+ A good proxy is the number of times that our Hamiltonian is applied, because this is often the most expensive
182
+ single operation.
183
+ To count this value, we write a custom "counting expression" that just measures how often the solver calls it.
184
+
185
+ ```{code-cell}
186
+ class CountingExpression(wp.expression.ExpressionBase):
187
+ def __init__(self, wrapped_expression):
188
+ super().__init__(False)
189
+
190
+ self._wrapped_expression = wrapped_expression
191
+ self.count = 0
192
+
193
+ def apply(self, state, t):
194
+ self.count += 1
195
+ return self._wrapped_expression.apply(state, t)
196
+ ```
197
+
198
+ Then we only wrap our (truncated) expression and propagate for a common time.
199
+
200
+ ```{code-cell}
201
+ counting_equation = CountingExpression(equation)
202
+
203
+ solver_chebychev = wp.solver.ChebychevSolver(counting_equation, math.pi/2, (0, 70))
204
+ solver_chebychev.step(psi0, t=0)
205
+ print(f"Chebychev solver: count={counting_equation.count}, alpha={solver_chebychev.alpha:.4}")
206
+
207
+ counting_equation.count = 0
208
+ solver_rk45 = wp.solver.OdeSolver(counting_equation, math.pi/2)
209
+ solver_rk45.step(psi0, t=0)
210
+ print(f"Runge-Kutta 4/5 solver: count={counting_equation.count}")
211
+
212
+ counting_equation.count = 0
213
+ solver_rk45_precise = wp.solver.OdeSolver(counting_equation, math.pi/2, rtol=1e-9, atol=1e-9)
214
+ solver_rk45_precise.step(psi0, t=0)
215
+ print(f"High-precision Runge-Kutta 4/5 solver: count={counting_equation.count}")
216
+ ```
217
+
218
+ So even when comparing against a low-precision ODE solver
219
+ (default 1e-6 for relative and absolute error of a single elementary time step),
220
+ the Chebychev solver yields a factor of 4.5 in performance.
221
+ If you require the higher precisions that you get for free with the Chebychev solver,
222
+ simple ODE solvers are easily an order of magnitude slower.
223
+
224
+ [^abramowitz]: M. Abramowitz and I. Stegun, "Handbook of Mathematical Functions"
225
+
226
+ [^ChebychevReal]: H. Tal Ezer and R. Kosloff, J. Chem. Phys. 81:3967 (1986)
227
+ <https://openscholar.huji.ac.il/sites/default/files/ronniekosloff/files/jcp1.448136.pdf>
228
+
229
+ [^ChebychevImag]: R. Kosloff and H. Tal-Ezer, Chem. Phys. Lett. 127(3) 223 (1986)
230
+ <https://openscholar.huji.ac.il/sites/default/files/ronniekosloff/files/cpl86.pdf>
231
+
232
+ [^faber]: W. Huisinga et al., J. Chem. Phys. 110, 5538 (1999)
233
+ <https://publications.imp.fu-berlin.de/91/1/JCP05538.pdf>