├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── example.py
├── examples
├── many_frequencies.py
└── many_frequencies.svg
├── matrixfuncs
├── __init__.py
├── _farray.py
├── _mspace.py
└── utils
│ ├── __init__.py
│ ├── _fcoeffs.py
│ └── _misc.py
├── pyproject.toml
├── sinFromMFunc.svg
├── tests
├── __init__.py
└── test_fcoeff.py
├── tox.ini
└── typst
├── .gitignore
├── eq.typ
├── github-logo.svg
├── matrix-functions-docu.typ
└── template.typ
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a report to help us improve
4 | title: "[Bug] Title"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Version**
11 | Package Version:
12 |
13 | **Describe the bug**
14 | A clear and concise description of what the bug is.
15 |
16 | **To Reproduce**
17 | Code to reproduce the behavior:
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Screenshots**
23 | If applicable, add screenshots to help explain your problem.
24 |
25 | **Additional context**
26 | Add any other context about the problem here.
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this project
4 | title: "[Suggestion] Title"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Suggestion**
11 | Detailed explanation of what feature you would like to have added.
12 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | # Controls when the workflow will run
4 | on:
5 | push:
6 | tags: [ "v*" ]
7 | #branches: [ "main" ]
8 | #pull_request:
9 | # branches: [ "main" ]
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
15 | jobs:
16 | test-matrix:
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | # python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14-dev' ]
21 | python-version: [ '3.10', '3.11', '3.12', '3.13' ]
22 | name: Test on ${{ matrix.python-version }}
23 | steps:
24 | - uses: actions/checkout@v4
25 | - name: Setup python
26 | uses: actions/setup-python@v5
27 | with:
28 | python-version: ${{ matrix.python-version }}
29 | cache: 'pip'
30 | cache-dependency-path: 'pyproject.toml'
31 | - name: Test python package
32 | run: |
33 | pip install --upgrade tox pytest
34 | TESTVERSION=${{ matrix.python-version }}
35 | TESTVERSION=$(python -c "s='$TESTVERSION'; print('py3'+s[2:].split('-')[0])")
36 | tox -e $TESTVERSION
37 |
38 | # act --pull=false -P ubuntu-latest=catthehacker/ubuntu:act-latest --job="build"
39 | build:
40 | runs-on: ubuntu-latest
41 | steps:
42 | - uses: actions/checkout@v4
43 |
44 | - name: Setup python
45 | uses: actions/setup-python@v5
46 | with:
47 | python-version: '3.10'
48 | cache: 'pip'
49 | cache-dependency-path: 'pyproject.toml'
50 |
51 | - name: Build python package
52 | run: |
53 | pip install --upgrade build
54 | python -m build
55 | PKGVERSION=$(python -c "$(grep version\ =\ matrixfuncs/_version.py); print(version)")
56 | echo "PKGVERSION=v$PKGVERSION" >> $GITHUB_ENV
57 | - name: "Report built version"
58 | run: echo "Built version ${{ env.PKGVERSION }}"
59 | - name: Upload package
60 | uses: actions/upload-artifact@v4
61 | with:
62 | name: python-package
63 | path: dist/*
64 |
65 | build-docs:
66 | runs-on: ubuntu-latest
67 | steps:
68 | - uses: actions/checkout@v4
69 | - name: Compile Typst
70 | uses: typst-community/setup-typst@v4
71 | - run: |
72 | cd typst
73 | typst compile --root .. matrix-functions-docu.typ
74 | cd
75 | - name: Upload documentation
76 | uses: actions/upload-artifact@v4
77 | with:
78 | name: python-package-documentation
79 | path: typst/matrix-functions-docu.pdf
80 |
81 | release:
82 | runs-on: ubuntu-latest
83 | if: github.triggering_actor == 'nextdorf'
84 | permissions:
85 | contents: write
86 | needs: [build, build-docs]
87 | steps:
88 | - name: Download
89 | uses: actions/download-artifact@v4
90 | with:
91 | path: out
92 | merge-multiple: true
93 | - name: Release
94 | uses: softprops/action-gh-release@v2
95 | with:
96 | files: out/*
97 | draft: false
98 | prerelease: false
99 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # custom
132 | local/
133 | matrixfuncs/_version.py
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Matrix Functions
2 |
3 | A Python package for **numerically computing matrix functions**, with a particular focus on **difference equations** and **analytic continuation of matrices**.
4 |
5 | ## Features
6 |
7 | - **General Matrix Functions** – Computes **any function** of a matrix.
8 | - **Numerical Computation** – Focuses on **floating-point approximations** rather than symbolic computation.
9 | - **Difference Equations** – Provides tools for solving **recurrence relations** using matrix function techniques.
10 | - **Analytic Continuation** – Enables non-integer shifts in difference equations using analytic continuation.
11 | - **Mathematical Documentation** – Each release includes a **PDF document** explaining the mathematical foundations.
12 | - **Pure Python** – No compilation required, making installation simple and cross-platform.
13 |
14 | ## Installation
15 |
16 | The package is available on PyPI and can be installed with:
17 |
18 | ```bash
19 | pip install matrixfuncs
20 | ```
21 |
22 | ###
23 |
24 | ## Usage
25 |
26 | The Cayley-Hamilton theorem states that every square matrix is a root of its own characteristic polynomial. From this it follows that $A^n$ is a linear combination of $A^0,\ A,\ A^2,\ \dots,\ A^{n-1}$ and therefore every polynomial in A is such a linear combination:
27 |
28 | $$
29 | A^m = \sum_{k=0}^{n-1} \alpha_{mk} A^k
30 | $$
31 |
32 | It turns out that $\alpha_{mk}$ only depends on the eigenvalues $\lambda_1,\ \dots,\ \lambda_n$. Hence, every matrix function can be expressed in in such a way if the function is analytic in the eigenvalues:
33 |
34 | $$
35 | f(A) = \varphi_{ij}^{(k)} A^i f^{(k)}(\lambda_j)
36 | $$
37 |
38 | ### Computing the Sine Function Using Matrix Recurrence Relations
39 |
40 | The sine function satisfies the recurrence relation:
41 |
42 | $$
43 | sin(x + a) = 2\cos(a) sin(x) - sin(x - a)
44 | $$
45 |
46 | This allows us to express the sine of a shifted angle using matrix multiplication:
47 |
48 | $$
49 | \begin{bmatrix} sin(x + a) \\ sin(x) \end{bmatrix} =
50 | \begin{bmatrix} 2\cos(a) & -1 \\ 1 & 0 \end{bmatrix}
51 | \begin{bmatrix} sin(x) \\ sin(x - a) \end{bmatrix}
52 | $$
53 |
54 |
55 |
56 | Using **matrix exponentiation**, we can compute \(sin(x + na)\) efficiently.
57 |
58 | #### Python Implementation
59 |
60 | ```python
61 | import matrixfuncs as mf
62 | import numpy as np
63 | import matplotlib.pyplot as plt
64 |
65 | # Define a transformation matrix based on a unit parameter
66 | unit = 0.2
67 | M = np.array([[2 * np.cos(unit), -1], [1, 0]])
68 |
69 | # Generate time steps for evaluation
70 | ts = np.linspace(0, 2 * np.pi, 1000)
71 |
72 | # Define the function to be applied to the matrix
73 | f = lambda x: x ** (ts / unit)
74 |
75 | # Convert the matrix into a functional array representation
76 | arr = mf.FArray.from_matrix(M)
77 |
78 | # Define input vectors for left-hand side and right-hand side multiplications
79 | v0_lhs = np.array([1, 0]) # Left-hand side vector
80 | v0_rhs = np.sin([0, -unit]) # Right-hand side vector
81 |
82 | # Compute the function applied to the matrix and evaluate it
83 | vals = v0_lhs @ arr @ v0_rhs # Compute matrix function application
84 | f_M = vals(f) # Evaluate the function over the time steps
85 |
86 |
87 | # Plot the computed function values
88 | fig = plt.figure(figsize=(8, 5))
89 | plt.plot(ts, f_M, 'b-', label='Continuation of sampled function')
90 | plt.plot(unit*np.arange(2*np.pi/unit), np.sin(unit*np.arange(2*np.pi/unit)), 'ro', label=f'Sampled at step size {unit}')
91 | plt.xlabel('Time ($t$)')
92 | plt.ylabel('$\\sin(t)$')
93 | plt.title(f'Smooth Continuaton of the Sine function with Step Size {unit}')
94 | plt.legend()
95 | plt.grid(True)
96 | plt.show(fig)
97 | fig.savefig('sinFromMFunc.png', dpi=200)
98 | ```
99 | 
100 |
101 |
102 | ## Documentation
103 |
104 | - A **PDF document** explaining the mathematical background is included with each release.
105 | - The package includes **in-code documentation** but no separate programming guide at this time.
106 |
107 | ## Benchmarks & Performance
108 |
109 | Currently, the package is focused on **numerical accuracy** rather than high-performance computing. Future updates will prioritize:
110 |
111 | - **Stability improvements** for larger matrices.
112 | - **Optimized numerical methods** to reduce floating-point errors.
113 | - **Support for large-scale computations**.
114 |
115 | ## Future Plans
116 |
117 | This package is **not yet feature-complete**. Future improvements will focus on:
118 |
119 | - Enhancing **numerical stability**.
120 | - Expanding support for **larger matrices**.
121 | - General optimizations and performance improvements.
122 |
123 | ## Contributing
124 |
125 | Contributions are welcome! If you'd like to help improve this package:
126 |
127 | 1. Fork the repository.
128 | 2. Create a new branch for your changes.
129 | 3. Submit a pull request with a clear description of your updates.
130 |
131 | Areas where contributions could be particularly helpful:
132 |
133 | - **Performance optimization**.
134 | - **Expanding function support**.
135 | - **Adding better documentation**.
136 |
137 | For feature suggestions or bug reports, please open an issue on GitHub.
138 |
139 | ## License
140 |
141 | This project is licensed under the **LGPL-3 License**. See the [LICENSE](LICENSE) file for details.
--------------------------------------------------------------------------------
/example.py:
--------------------------------------------------------------------------------
1 | import matrixfuncs as mf
2 | import numpy as np
3 | import matplotlib.pyplot as plt
4 |
5 | # Define a transformation matrix based on a unit parameter
6 | unit = 0.2
7 | M = np.array([[2 * np.cos(unit), -1], [1, 0]])
8 |
9 | # Generate time steps for evaluation
10 | ts = np.linspace(0, 2 * np.pi, 1000)
11 |
12 | # Define the function to be applied to the matrix
13 | f = lambda x: x ** (ts / unit)
14 |
15 | # Convert the matrix into a functional array representation
16 | arr = mf.FArray.from_matrix(M)
17 |
18 | # Define input vectors for left-hand side and right-hand side multiplications
19 | v0_lhs = np.array([1, 0]) # Left-hand side vector
20 | v0_rhs = np.sin([0, -unit]) # Right-hand side vector
21 |
22 | # Compute the function applied to the matrix and evaluate it
23 | vals = v0_lhs @ arr @ v0_rhs # Compute matrix function application
24 | f_M = vals(f) # Evaluate the function over the time steps
25 |
26 |
27 | # Plot the computed function values
28 | fig = plt.figure(figsize=(8, 5))
29 | plt.plot(ts, f_M, 'b-', label='Continuation of sampled function')
30 | plt.plot(unit*np.arange(2*np.pi/unit), np.sin(unit*np.arange(2*np.pi/unit)), 'ro', label=f'Sampled at step size {unit}')
31 | plt.xlabel('Time ($t$)')
32 | plt.ylabel('$\\sin(t)$')
33 | plt.title(f'Smooth Continuaton of the Sine function with Step Size {unit}')
34 | plt.legend()
35 | plt.grid(True)
36 | plt.show(fig)
37 | fig.savefig('sinFromMFunc.svg', dpi=200)
38 |
--------------------------------------------------------------------------------
/examples/many_frequencies.py:
--------------------------------------------------------------------------------
1 | import matrixfuncs as mf
2 | import numpy as np
3 | import matplotlib.pyplot as plt
4 |
5 |
6 | def est_M(sampled_vec):
7 | '''Estimate transition matrix M from sampled function values.
8 |
9 | Parameters
10 | ----------
11 | sampled_vec : ndarray of shape (dim, N)
12 | A matrix where each column represents function values over a time window.
13 |
14 | Returns
15 | -------
16 | M : ndarray of shape (dim, dim)
17 | The estimated transition matrix that best maps sampled_vec[:, :-1] to sampled_vec[:, 1:].
18 | '''
19 | dim = sampled_vec.shape[0]
20 | r = range(sampled_vec.shape[1] - dim)
21 | Ms = [np.linalg.solve(sampled_vec.T[k:k+dim], sampled_vec.T[k+1:k+1+dim]).T for k in r]
22 | M = np.mean(Ms, 0)
23 | return M
24 |
25 |
26 | # Number of frequency components
27 | num = 4
28 |
29 | # Generate random function coefficients and frequencies
30 | f_coeffs = np.random.randn(num)
31 | f_freqs = 2*np.pi/(.1 + .9*np.random.rand(num))
32 |
33 | # Define the reference function as a weighted sum of sines and cosines
34 | f_ref = lambda t: f_coeffs @ np.concat([np.sin(f_freqs[:num//2]*t), np.cos(f_freqs[num//2:]*t)])
35 |
36 | # Sample function values over a finite time range
37 | t_sampled = np.linspace(0, 1, 5*num+1)
38 | f_sampled = np.array(list(map(f_ref, t_sampled)))
39 |
40 | # Construct sampled windows for transition matrix estimation
41 | f_sampled_window = np.array([f_sampled[np.arange(i, i+2*num)] for i in range(len(t_sampled) - 2*num)]).T
42 | M = est_M(f_sampled_window)
43 | assert np.allclose(M @ f_sampled_window[:, :-1], f_sampled_window[:, 1:])
44 |
45 |
46 | # Define evaluation times for function continuation
47 | ts = np.linspace(-1, 2, 4000)
48 | dt_sample = t_sampled[1] - t_sampled[0]
49 | assert np.allclose(t_sampled[1:], t_sampled[:-1] + dt_sample)
50 |
51 | # Define the function to be applied to the matrix
52 | f = lambda x: x ** (ts / dt_sample)
53 |
54 | # Convert the transition matrix into a functional array representation
55 | arr = mf.FArray.from_matrix(M)
56 |
57 | # Define initial left-hand side and right-hand side vectors
58 | v0_lhs = (np.arange(f_sampled_window.shape[0])==0)*1.
59 | v0_rhs = f_sampled_window[:, 0]
60 |
61 | # Compute the function applied to the matrix and evaluate it
62 | vals = v0_lhs @ arr @ v0_rhs # Compute matrix function application
63 | f_M = vals(f) # Evaluate the function over the time steps
64 |
65 |
66 | # Plot the computed function values
67 | fig, (ax, bx, cx) = plt.subplots(3, figsize=(8, 9), gridspec_kw={'height_ratios': [2, 1, 1]})
68 | fig.suptitle(f'Smooth Continuation of Random Signal with DOF={2*num}')
69 | fig.tight_layout(pad=1.)
70 |
71 | # Plot the reconstructed function
72 | ax.plot(ts, f_M, 'b-', label='Continuation of sampled function')
73 | ax.plot(t_sampled, f_sampled, 'ro', label=f'Sampled at step size {dt_sample}')
74 | ax.fill_betweenx((-1e8, 1e8), 0, 1, color='r', alpha=.2, label=f'Sampled region')
75 | ax.set_xlim(-1, 2)
76 | ax.set_ylim(np.array([np.min(f_M), np.max(f_M)]) * 1.05)
77 | ax.set_ylabel('Signal')
78 | ax.legend()
79 | ax.grid(True)
80 |
81 | # Compute error between estimated function and reference function
82 | err1 = f_M - np.array([f_ref(t) for t in ts])
83 |
84 | # Plot the error
85 | bx.plot(ts, err1, 'b-', label='Error')
86 | bx.plot(ts, np.zeros_like(ts), 'g-')
87 | bx.fill_betweenx((-1e8, 1e8), 0, 1, color='r', alpha=.2)
88 | bx.sharex(ax)
89 | bx.set_ylim(np.array([-1.05, 1.05]) * np.max(abs(err1)))
90 | bx.set_ylabel('Signal Error')
91 | bx.legend()
92 | bx.grid(True)
93 |
94 | # Repeat on bigger scale
95 | ts = np.linspace(-50, 50, 4000)
96 | f = lambda x: x ** (ts / dt_sample)
97 | f_M = vals(f)
98 | err2 = f_M - np.array([f_ref(t) for t in ts])
99 |
100 | cx.plot(ts, err2, 'b-', label='Error')
101 | cx.plot(ts, np.zeros_like(ts), 'g-')
102 | cx.fill_betweenx((-1e8, 1e8), 0, 1, color='r', alpha=.2)
103 | cx.set_xlim((min(ts), max(ts)))
104 | cx.set_ylim(np.array([-1.05, 1.05]) * np.max(abs(err2)))
105 | cx.set_xlabel('Time ($t$)')
106 | cx.set_ylabel('Signal Error')
107 | cx.legend()
108 | cx.grid(True)
109 |
110 |
111 | # Display and save the plot
112 | plt.show(fig)
113 | fig.savefig('many_frequencies.svg', dpi=200, bbox_inches='tight')
114 |
--------------------------------------------------------------------------------
/matrixfuncs/__init__.py:
--------------------------------------------------------------------------------
1 | from . import utils
2 | from .utils import apply_fn, Multiplicity
3 | from ._mspace import MSpace
4 | from ._farray import FArray
5 |
6 |
--------------------------------------------------------------------------------
/matrixfuncs/_farray.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import scipy
3 | from . import MSpace, apply_fn
4 |
5 | class FArray:
6 | '''Represents a functional array associated with a matrix space.
7 |
8 | This class enables efficient function application to matrices using precomputed function coefficients.
9 |
10 | Attributes
11 | ----------
12 | space : MSpace
13 | The underlying matrix space associated with the functional array.
14 | mult_space : str
15 | Defines the multiplication space ('min' by default).
16 | bare : np.ndarray
17 | The function coefficients tensor. If not provided, it is copied from `space.f_coeffs`.
18 | '''
19 | __array_priority__ = 1
20 | def __init__(self, space: MSpace, mult_space='min', bare=None, stay_lifted=True):
21 | '''
22 | Initialize an FArray instance.
23 |
24 | Parameters
25 | ----------
26 | space : MSpace
27 | The matrix space associated with this functional array.
28 | mult_space : str, optional
29 | The multiplication space, default is 'min'.
30 | bare : np.ndarray, optional
31 | Precomputed function coefficients. Defaults to `space.f_coeffs` if not provided.
32 | stay_lifted : bool, optional
33 | Whether the object remains lifted when applying functions.
34 | '''
35 | self.space = space
36 | self.mult_space=mult_space
37 | self.bare = np.copy(space.f_coeffs) if bare is None else bare
38 | self.__stay_lifted = stay_lifted
39 |
40 | @staticmethod
41 | def from_matrix(M: np.ndarray, **kwargs):
42 | '''
43 | Create an FArray instance from a matrix.
44 |
45 | Parameters
46 | ----------
47 | M : np.ndarray
48 | The input matrix.
49 | **kwargs : dict
50 | Optional keyword arguments, including:
51 | - eigvals: Precomputed eigenvalues.
52 | - mult_space: Multiplication space setting.
53 | - bare: Precomputed function coefficients.
54 | - stay_lifted: Whether the instance should stay lifted.
55 |
56 | Returns
57 | -------
58 | FArray
59 | An instance of the functional array.
60 | '''
61 | _kwargs = {k: kwargs[k] for k in 'eigvals'.split() if k in kwargs}
62 | space = MSpace(M, **_kwargs)
63 | _kwargs = {k: kwargs[k] for k in 'mult_space bare stay_lifted'.split() if k in kwargs}
64 | return FArray(space, **_kwargs)
65 |
66 | def __call__(self, f, *dfs, gen_df=None, stay_lifted=None, real_if_close='auto', **kwargs):
67 | if isinstance(real_if_close, str):
68 | if real_if_close == 'auto':
69 | real_if_close = not np.iscomplexobj(self.space.M)
70 | else:
71 | raise ValueError(f'Unsupported value for real_if_close: {real_if_close}')
72 |
73 | ret = apply_fn(M=None, f=f, dfs=dfs, gen_df=gen_df, eigvals=self.space.multiplicity, coeffs=self.bare, mult_space=self.mult_space, real_if_close=real_if_close, **kwargs)
74 |
75 | ret_shape = np.shape(ret)
76 | is_squre_matrix = len(ret_shape)==2 and ret_shape[0] == ret_shape[1]
77 | if stay_lifted is None:
78 | stay_lifted = self.__stay_lifted and is_squre_matrix
79 | elif stay_lifted == True:
80 | stay_lifted = is_squre_matrix
81 | else:
82 | stay_lifted = False
83 |
84 | if stay_lifted:
85 | f_space = MSpace(ret)
86 | arr = FArray(f_space, mult_space=self.mult_space)
87 | return arr
88 | else:
89 | return ret
90 |
91 | @property
92 | def cs_shape(self):
93 | return np.shape(self.bare)[1:]
94 | @property
95 | def f_shape(self):
96 | return np.shape(self.bare)[0]
97 | @property
98 | def shape(self):
99 | return np.shape(self.bare)
100 |
101 |
102 | def __matmul__coeffs(self, other):
103 | if self.appliedFunc:
104 | coeffs = np.tensordot(self.coeffs, other, ([-1], [0]))
105 | if isinstance(coeffs, type(NotImplemented)):
106 | return coeffs
107 | else:
108 | coeffs = np.tensordot(self.coeffs, other, ([-2], [0]))
109 | if isinstance(coeffs, type(NotImplemented)):
110 | return coeffs
111 | oldRank = len(self.coeffs.shape)
112 | newRank = len(coeffs.shape)
113 | if oldRank-1 != newRank:
114 | coeffs = np.moveaxis(coeffs, oldRank-2, -1)
115 | return coeffs
116 | def __matmul__(self, other):
117 | if not bool(self.cs_shape):
118 | return NotImplemented
119 | cs = np.tensordot(self.bare, other, 1)
120 | if cs is NotImplemented:
121 | return NotImplemented
122 | ret = FArray(self.space, mult_space=self.mult_space, bare=cs, stay_lifted=False)
123 | return ret
124 | def __rmatmul__(self, other):
125 | if not bool(self.cs_shape):
126 | return NotImplemented
127 | cs = np.tensordot(other, self.bare, ((-1,), (1,)))
128 | if cs is NotImplemented:
129 | return NotImplemented
130 | cs = np.moveaxis(cs, -len(self.cs_shape), 0)
131 | ret = FArray(self.space, mult_space=self.mult_space, bare=cs, stay_lifted=False)
132 | return ret
133 | def __imatmul__(self, other):
134 | if not bool(self.cs_shape):
135 | return NotImplemented
136 | cs = np.tensordot(self.bare, other, 1)
137 | if cs is NotImplemented:
138 | return NotImplemented
139 | self.bare = cs
140 | self.__stay_lifted = False
141 | return self
142 |
143 | def __repr__(self):
144 | ret = repr(self.bare)
145 | main_idx = ret.find('(')
146 | return f'FArray{ret[main_idx:]}'
147 |
--------------------------------------------------------------------------------
/matrixfuncs/_mspace.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from collections import namedtuple
3 | import scipy
4 | from .utils import matrix_power_series, Multiplicity, function_coeffs, apply_fn, err
5 | from warnings import warn
6 |
7 | def bare_coeffs(tensor: np.ndarray, basis: np.ndarray, assume_normed = False, normalize_to: float|None=1.):
8 | '''Compute expansion coefficients of `tensor` in terms of `basis` vectors.
9 |
10 | Parameters
11 | ----------
12 | tensor : np.ndarray
13 | The input tensor to decompose.
14 | basis : np.ndarray
15 | The basis vectors (assumed to be square matrices).
16 | assume_normed : bool, optional
17 | If True, assumes that `basis` is already orthonormal, avoiding unnecessary computations.
18 | normalize_to : float or None, optional
19 | If not None, rescales `basis` to have a norm of `normalize_to`.
20 |
21 | Returns
22 | -------
23 | np.ndarray
24 | The expansion coefficients of `tensor` in the `basis`.
25 | '''
26 |
27 | #TODO This function is unreasonably more stable for normed basis. I suspect a bug somewhere
28 | if normalize_to is not None and not assume_normed:
29 | # prod_norms = []
30 | # for evs in abs(np.linalg.eigvals(basis)):
31 | # non_zero = evs[evs > 1e-15]
32 | # dim = len(non_zero)
33 | # norm = np.prod(non_zero**(1/dim)) if dim > 0 else 1
34 | # prod_norms.append(norm)
35 | # # prod_norms = np.reshape(prod_norms, (-1, ) + (1,)*(len(np.shape(basis))-1))
36 | # prod_norms = np.array(prod_norms)
37 | # scale = np.reshape(prod_norms / normalize_to, (-1, 1, 1))
38 | norms = MSpace.norm(basis)
39 | scale = np.reshape(norms / normalize_to, (-1, 1, 1))
40 | scaled_cs = bare_coeffs(tensor, basis / scale, assume_normed=assume_normed, normalize_to=None)
41 | cs = scaled_cs / np.reshape(scale, (1, -1))
42 | else:
43 | cs = MSpace.sdot(basis, tensor)
44 | if not assume_normed:
45 | basis_change = MSpace.sdot(basis, basis)
46 | # cs = scipy.linalg.solve(basis_change, cs, assume_a='hermitian')
47 |
48 | # TODO: Check if this improves the accuracy problem for near-singular matrices
49 | cond_number = np.linalg.cond(basis_change) # A test to check whether basis_change is almost singular
50 | if cond_number > 1e12:
51 | cs = np.linalg.pinv(basis_change) @ cs
52 | else:
53 | cs = scipy.linalg.solve(basis_change, cs, assume_a='hermitian')
54 |
55 | cs = np.moveaxis(cs, 0, -1)
56 | return cs
57 |
58 |
59 |
60 | class MSpace:
61 | '''Represents a matrix space generated by a given square matrix.
62 |
63 | This class provides methods to compute various objects related to the vector space generated by a matrix, including
64 | an orthonormal basis, eigenvalues, function coefficient tensors, and transformations between different basis representations.
65 |
66 | Attributes
67 | ----------
68 | M : np.ndarray
69 | The input square matrix that generates the space.
70 | eigvals : np.ndarray
71 | The eigenvalues of `M`.
72 | normed_basis : np.ndarray
73 | The orthonormal basis.
74 | multiplicity : Multiplicity
75 | The (algebraic and geometric) multiplicity of the eigenvalues.
76 | basis : np.ndarray
77 | The basis of the space generated by `M` repeatedly multiplying `M` with itself.
78 | normed_to_basis : np.ndarray
79 | Transformation matrix from the orthonormal basis to the generated basis.
80 | basis_to_normed : np.ndarray
81 | Inverse transformation matrix from the generated basis to the orthonormal basis.
82 | f_coeffs : np.ndarray
83 | Function coefficients for computing functions of `M`, i.e. `tensordot(multiplicity.map(f), f_coeffs, 1) = f(M)`.
84 | phi_coeffs_normed : np.ndarray
85 | Phi coefficients expressed in the orthonormal basis, i.e. `tensordot(phi_coeffs_normed, normed_basis, 1) = f_coeffs`.
86 | phi_coeffs : np.ndarray
87 | Phi coefficients expressed in the generated basis, i.e. `tensordot(phi_coeffs, basis, 1) = f_coeffs`.
88 | '''
89 |
90 | def __init__(self, M: np.ndarray, eigvals: np.ndarray|Multiplicity|None = None):
91 | '''
92 | Parameters
93 | ----------
94 | M : np.ndarray
95 | A square matrix generating the space.
96 | eigvals : np.ndarray, Multiplicity, or None, optional
97 | If provided, these are the eigenvalues of `M`. If `None`, they are computed automatically.
98 | '''
99 | def test(name:str, val, ref):
100 | if not np.allclose(val, ref):
101 | err_val = err(val, ref)
102 | warn(f'Error of {name} is high, relative error is {err_val}')
103 |
104 | self.M = np.array(M)
105 | self.eigvals = (
106 | np.linalg.eigvals(self.M) if eigvals is None
107 | else eigvals.full_eigvals if isinstance(eigvals, Multiplicity)
108 | else eigvals
109 | )
110 | self.__normed_basis = MSpace.Normed_Basis(self.M)
111 | self.multiplicity = Multiplicity.from_matrix(self.M, self.eigvals) if not isinstance(eigvals, Multiplicity) else eigvals
112 | dim = self.dim
113 | _dim = np.sum(self.multiplicity.algebraic - self.multiplicity.geometric + 1)
114 | assert dim == _dim, 'Inconsistent dimension count between basis dimension and eigenvalue multiplicity'
115 | self.basis = matrix_power_series(self.M, dim)
116 |
117 | self.normed_to_basis = MSpace.sdot(self.normed_basis, self.basis)
118 | self.basis_to_normed = scipy.linalg.inv(self.normed_to_basis)
119 |
120 | self.f_coeffs, _ = function_coeffs(self.M, self.multiplicity)
121 | self.phi_coeffs_normed = bare_coeffs(self.f_coeffs, self.normed_basis, assume_normed=True)
122 | self.phi_coeffs = np.tensordot(self.phi_coeffs_normed, self.basis_to_normed, ((1, ), (1,)))
123 |
124 | _basis = np.tensordot(self.normed_to_basis, self.normed_basis, ((0,), (0,)))
125 | test('normed_to_basis', self.basis, _basis)
126 | _n_basis = np.tensordot(self.basis_to_normed, self.basis, ((0,), (0,)))
127 | test('basis_to_normed', self.normed_basis, _n_basis)
128 | _fcoeffs_n = np.tensordot(self.phi_coeffs_normed, self.normed_basis, 1)
129 | test('phi_coeffs_normed', self.f_coeffs, _fcoeffs_n)
130 | _fcoeffs = np.tensordot(self.phi_coeffs, self.basis, 1)
131 | test('phi_coeffs', self.f_coeffs, _fcoeffs)
132 |
133 | @property
134 | def normed_basis(self):
135 | 'Orthonormal Basis'
136 | return self.__normed_basis
137 |
138 | @property
139 | def dim(self):
140 | 'Dimension of the space (rank of the matrix)'
141 | return len(self.normed_basis)
142 |
143 | @staticmethod
144 | def sdot(x: np.ndarray, y: np.ndarray):
145 | '''Compute the scaled inner product (dot product) of two square matrices.
146 |
147 | Parameters
148 | ----------
149 | x : np.ndarray
150 | A square matrix.
151 | y : np.ndarray
152 | A square matrix of the same size as `x`.
153 |
154 | Returns
155 | -------
156 | np.ndarray
157 | The scaled inner product of `x` and `y`, defined as `tr(x^t y)/n`
158 |
159 | Raises
160 | ------
161 | AssertionError
162 | If `x` and `y` are not square matrices of the same size.
163 | '''
164 | shapes = np.shape(x)[-2:] + np.shape(y)[-2:]
165 | n = shapes[0]
166 | assert shapes == (n, )*4, 'x and y have to be square matrices of the same size'
167 | ret = np.tensordot(np.conj(x), y, ((-2, -1), (-2, -1)))/n
168 | return ret
169 |
170 | @staticmethod
171 | def norm(x: np.ndarray):
172 | 'Normalized Frobenius norm of the matrix `x`'
173 | return np.linalg.norm(x, axis=(-2, -1))/np.sqrt(np.shape(x)[-1])
174 |
175 | @staticmethod
176 | def Normed_Basis(M: np.ndarray, repeat_count=1):
177 | '''Compute an orthonormal basis of matrices using the Gram-Schmidt process.
178 |
179 | This function applies the Gram-Schmidt (GS) orthogonalization method to the input matrix `M`,
180 | ensuring that the resulting vectors form an orthonormal basis. The method starts with the the
181 | identity matrix, and generates the next basis element by multiplying the last basis element
182 | with `M` and subtracting the span of the previous basis elements. Optionally, the process can
183 | be repeated multiple times for increased numerical stability.
184 |
185 | Parameters
186 | ----------
187 | M : np.ndarray
188 | The input matrix whose column vectors are to be orthonormalized.
189 | repeat_count : int, optional (default=1)
190 | The number of times the Gram-Schmidt process is applied. If `repeat_count` is greater than 1,
191 | the method is reapplied to refine the basis. However, applying GS multiple times generally does
192 | not significantly improve accuracy.
193 |
194 | Returns
195 | -------
196 | np.ndarray
197 | A 2D array where each row represents an orthonormal basis vector.
198 |
199 | Notes
200 | -----
201 | - If the input matrix contains linearly dependent vectors, the basis may have fewer vectors than `M.shape[-1]`.
202 | - The method relies on the custom `MSpace.sdot` and `MSpace.norm` functions for inner products and vector norms.
203 | '''
204 |
205 | ret: list[np.ndarray] = [np.eye(M.shape[-1])]
206 |
207 | # First pass of Gram-Schmidt orthogonalization
208 | for _ in range(1, M.shape[-1]):
209 | new_val0 = ret[-1] @ M
210 | parallels = MSpace.sdot(ret, new_val0)
211 | paralell_vec = np.tensordot(ret, parallels, ((0,), (0,)))
212 | perpendicular_vec = new_val0 - paralell_vec
213 | norm = MSpace.norm(perpendicular_vec)
214 | if np.allclose(norm, 0):
215 | break
216 | ret.append(perpendicular_vec / norm)
217 |
218 | # Optional: Repeat Gram-Schmidt process for refinement
219 | for _ in range(repeat_count - 1):
220 | if len(ret) > 2:
221 | break
222 | ret0 = ret
223 | ret = ret0[:2]
224 | for next_val in ret0[2:]:
225 | parallels = MSpace.sdot(ret, next_val)
226 | paralell_vec = np.tensordot(ret, parallels, ((0,), (0,)))
227 | perpendicular_vec = next_val - paralell_vec
228 | norm = MSpace.norm(perpendicular_vec)
229 | if np.allclose(norm, 0):
230 | break
231 |
232 | ret.append(perpendicular_vec / norm)
233 |
234 | return np.array(ret)
235 |
236 | def __call__(self, f, *dfs, gen_df=None, **kwargs):
237 | 'See `apply_fn`'
238 | return apply_fn(M=None, f=f, dfs=dfs, gen_df=gen_df, eigvals=self.multiplicity, coeffs=self.f_coeffs, mult_space='min', **kwargs)
239 |
240 | def __repr__(self) -> str:
241 | ident = ' '*2
242 | inner = ident + f',\n{ident}'.join((
243 | f'M: {self.M}',
244 | f'multiplicity: {self.multiplicity}',
245 | f'dim: {self.dim}',
246 | f'basis: {self.basis}',
247 | f'coeffs: {self.phi_coeffs}'
248 | ))
249 | return f'MSpace(\n{inner} )'
250 |
--------------------------------------------------------------------------------
/matrixfuncs/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from ._fcoeffs import (
2 | apply_fn,
3 | b_matrix,
4 | eigval_multiplicity,
5 | function_coeffs,
6 | matrix_power_series,
7 | Multiplicity
8 | )
9 |
10 | from ._misc import err
11 |
12 |
--------------------------------------------------------------------------------
/matrixfuncs/utils/_fcoeffs.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from collections import namedtuple
3 | import numdifftools as nd
4 | import scipy
5 |
6 | class Multiplicity(namedtuple('Multiplicity', 'eigvals algebraic geometric'.split())):
7 | '''A named tuple representing the multiplicities of eigenvalues in a matrix.
8 |
9 | This class stores distinct eigenvalues along with their algebraic and geometric multiplicities.
10 | It provides methods for applying functions to eigenvalues and computing properties like trace,
11 | determinant, rank, and a normalization factor.
12 |
13 | Attributes
14 | ----------
15 | eigvals : np.ndarray
16 | An array containing distinct eigenvalues of the matrix.
17 | algebraic : np.ndarray
18 | The algebraic multiplicity of each corresponding eigenvalue.
19 | geometric : np.ndarray
20 | The geometric multiplicity of each corresponding eigenvalue.
21 | '''
22 | def __new__(cls, eigvals: np.ndarray, algebraic: np.ndarray, geometric: np.ndarray):
23 | '''Constructor for the Multiplicity class.
24 |
25 | Parameters
26 | ----------
27 | eigvals : np.ndarray
28 | Distinct eigenvalues of the matrix.
29 | algebraic : np.ndarray
30 | Algebraic multiplicities of each eigenvalue.
31 | geometric : np.ndarray
32 | Geometric multiplicities of each eigenvalue.
33 |
34 | Returns
35 | -------
36 | Multiplicity
37 | A named tuple containing eigenvalues and their multiplicities.
38 | '''
39 | return super(Multiplicity, cls).__new__(cls, np.asarray(eigvals), np.asarray(algebraic), np.asarray(geometric))
40 |
41 | def map(self, f, *dfs, gen_df=None, mult_space='min', **kwargs):
42 | '''Maps a function (and derivatives) to the eigenvalues.
43 |
44 | This method applies `f` and its derivatives to each eigenvalue according to its
45 | algebraic multiplicity.
46 |
47 | Parameters
48 | ----------
49 | f : function
50 | The function to apply to the eigenvalues.
51 | dfs : tuple of functions, optional
52 | Precomputed derivatives of `f`, if available.
53 | mult_space : 'min' | 'full'
54 | See `Multiplicity.ev_iter` for details.
55 | gen_df : function, optional
56 | A function that generates higher-order derivatives when needed. gen_df(k) should generate
57 | the kth derivative. Using dfs takes precedance over using gen_df. If gen_df is `None` then
58 | `numdifftools.Derivative` is used.
59 |
60 | Returns
61 | -------
62 | ndarray
63 | (f(ev0), f'(ev0), f''(ev0), ..., f(ev1), f'(ev1), f''(ev1), ...)
64 |
65 | Notes
66 | -----
67 | See also `ev_iter` for further details on the order.
68 | '''
69 | diff_count = np.max(self.algebraic)
70 | fs = [f] + list(dfs[:diff_count - 1])
71 |
72 | if len(fs) < diff_count:
73 | if gen_df is None:
74 | k0 = len(fs) - 1
75 | gen_df = lambda i, **_: nd.Derivative(fs[k0], n=i - k0, order=2 * 1)
76 | for i in range(len(fs), diff_count):
77 | fs.append(gen_df(i))
78 |
79 | #TODO: vectorize
80 | ev_iter = self.ev_iter(mult_space)
81 | ret = [fs[k](ev) for _, ev, _, k in ev_iter]
82 | return np.array(ret)
83 |
84 | def ev_iter(self, mult_space='min'):
85 | '''Iterate over eigenvalues and their multiplicities.
86 |
87 | Parameters
88 | ----------
89 | mult_space : 'min' | 'full'
90 | - if 'full' then iterate through algebraic multiplicity
91 | - if 'min' then iterate algebraic multiplicity of irreducable representation
92 |
93 |
94 | Returns
95 | -------
96 | generator
97 | Yields (eigenvalue index, eigenvalue, multiplicity, counter of the multiplicity)
98 |
99 | The inner iteration is through the multiplicity and the outer iterator is through the
100 | eigenvalues.
101 | '''
102 | return ((i, ev, mult, k) for i, (ev, mult) in enumerate(zip(self.eigvals, self._multiplicity(mult_space))) for k in range(mult))
103 |
104 | def _multiplicity(self, mult_space='min'):
105 | '''Effective multiplicity
106 |
107 | Parameters
108 | ----------
109 | mult_space : 'min' | 'full'
110 | - if 'full' then return algebraic multiplicity
111 | - if 'min' then return algebraic multiplicity of irreducable representation
112 |
113 | Returns
114 | -------
115 | ndarray
116 | '''
117 | if mult_space == 'min':
118 | return self.algebraic - self.geometric + 1
119 | elif mult_space == 'full':
120 | return self.algebraic
121 | else:
122 | raise ValueError(f'Unsupported mult_space "{mult_space}"')
123 |
124 |
125 | @property
126 | def tr(self):
127 | 'The trace'
128 | return self.eigvals @ self.algebraic
129 |
130 | @property
131 | def full_eigvals(self):
132 | 'All eigenvalues'
133 | return np.array([ev for _,ev,_,_ in self.ev_iter('full')])
134 |
135 | @property
136 | def det(self):
137 | 'The determinant'
138 | return np.prod(self.eigvals ** self.algebraic)
139 |
140 | @property
141 | def dim(self):
142 | 'The dimension'
143 | return np.sum(self.algebraic)
144 |
145 | @property
146 | def rank(self):
147 | 'The rank'
148 | return np.sum(self.algebraic - self.geometric + 1)
149 |
150 | def product_norm(self, mult_space='min'):
151 | '''Compute a norm-like quantity used for matrix normalization.
152 |
153 | - If all eigenvalues are nonzero, returns `|det|^(1/dim)`.
154 | - If some but not all eigenvalues are nonzero, returns `product_norm` of the nonzero part.
155 | - If the only eigenvalue is 0, returns 1.
156 |
157 | Parameters
158 | ----------
159 | mult_space : 'min' | 'full'
160 | See `Multiplicity.ev_iter` for details.
161 |
162 | Returns
163 | -------
164 | float
165 | Computed normalization value.
166 | '''
167 | if np.all(self.eigvals == 0):
168 | return np.ones_like(self.eigvals[0])
169 | elif np.any(self.eigvals == 0):
170 | inds = self.eigvals != 0
171 | return Multiplicity(*(q[inds] for q in self)).product_norm(mult_space)
172 | else:
173 | mult = self._multiplicity(mult_space)
174 | e = mult / np.sum(mult)
175 | return np.prod(np.abs(self.eigvals) ** e)
176 |
177 | @staticmethod
178 | def from_matrix(M: np.ndarray, eigvals: np.ndarray | None = None, **kwargs):
179 | '''Create a `Multiplicity` instance from a given matrix.
180 |
181 | Parameters
182 | ----------
183 | M : np.ndarray
184 | The input matrix.
185 | eigvals : np.ndarray, optional
186 | Precomputed eigenvalues. If `None`, they are computed automatically.
187 |
188 | Returns
189 | -------
190 | Multiplicity
191 | A `Multiplicity` instance containing eigenvalue data.
192 |
193 | See also
194 | --------
195 | `eigval_multiplicity`
196 | '''
197 | return eigval_multiplicity(M=M, eigvals=eigvals, **kwargs)
198 |
199 |
200 | def matrix_power_series(M: np.ndarray, stop: int):
201 | '''Compute a sequence of matrix powers up to a given order.
202 |
203 | This function efficiently computes powers of a square matrix up to `stop - 1` using recursive
204 | squaring to reduce computational complexity.
205 |
206 | Parameters
207 | ----------
208 | M : np.ndarray
209 | A square matrix whose powers need to be computed.
210 | stop : int
211 | The exclusive upper bound to compute in the series.
212 |
213 | Returns
214 | -------
215 | np.ndarray
216 | An array containing matrices from the identity matrix (I) to M^(stop-1).
217 | '''
218 | shape = np.shape(M)
219 | assert len(shape)==2 and shape[0]==shape[1], 'M is not a square matrix'
220 | dim = shape[0]
221 | ret = [np.eye(dim, dtype=M.dtype), M]
222 | for i in range(2, stop):
223 | k1 = i // 2
224 | k2 = i - k1
225 | ret.append(ret[k1] @ ret[k2])
226 | return np.array(ret)[:max(stop, 0)]
227 |
228 | def eigval_multiplicity(M: np.ndarray, eigvals: np.ndarray | None =None, zero_thrsh = 1e-15, rel_eq_thrsh = 1e-8):
229 | '''Compute the algebraic and geometric multiplicities of eigenvalues.
230 |
231 | This function determines the distinct eigenvalues of a given matrix, their algebraic
232 | multiplicities, and geometric multiplicities (dimension of the eigenspaces).
233 |
234 | Parameters
235 | ----------
236 | M : np.ndarray
237 | A square matrix whose eigenvalues are analyzed.
238 | eigvals : np.ndarray, optional
239 | Precomputed eigenvalues of the matrix. If None, they are computed internally.
240 | zero_thrsh : float = 1e-15
241 | Threshold below which eigenvalues are considered zero. Set to 0 if only 0 itself should be
242 | considered a vanishing eigenvalue.
243 | rel_eq_thrsh : float = 1e-8
244 | Relative threshold for treating eigenvalues as identical.
245 |
246 | Returns
247 | -------
248 | mult: Multiplicity
249 | '''
250 | # TODO: This seems to be extremely instable for eigenvalues which are close but not equal, however fcoeffs.dim seems to do a good job in finding the dimension
251 | if eigvals is None:
252 | eigvals = np.linalg.eigvals(M)
253 | else:
254 | eigvals = np.array(eigvals)
255 | nEigvals = eigvals.shape[0]
256 | non_zero_eigvals = eigvals[abs(eigvals) > zero_thrsh]
257 | unique_eigvals = [0*eigvals[0]] if non_zero_eigvals.shape != eigvals.shape else []
258 | for ev in non_zero_eigvals:
259 | differs = abs(np.array(unique_eigvals)/ev - 1) > rel_eq_thrsh
260 | if differs.all():
261 | unique_eigvals.append(ev)
262 | unique_eigvals = np.array(unique_eigvals)
263 |
264 | alg_mult = [0]*len(unique_eigvals)
265 | for ev in eigvals:
266 | alg_mult[abs(unique_eigvals - ev).argmin()] += 1
267 | alg_mult = np.array(alg_mult)
268 |
269 | geom_mult = []
270 | for i, ev in enumerate(unique_eigvals):
271 | if len(unique_eigvals) == 1:
272 | ev_thrsh = np.inf
273 | else:
274 | other_eigvals = unique_eigvals[np.arange(len(unique_eigvals)) != i]
275 | ev_thrsh = abs(other_eigvals - ev).min() / 2
276 | eigvals_ker, eigvecs_ker = np.linalg.eig(M - ev*np.eye(nEigvals))
277 | inds_ker = abs(eigvals_ker) < ev_thrsh
278 | vecs_ker = eigvecs_ker[:, inds_ker]
279 | # vecs_ker_min = normed_basis(vecs_ker.T).T
280 | vecs_ker_min = scipy.linalg.orth(vecs_ker)
281 | geom_mult.append(np.shape(vecs_ker_min)[-1])
282 | geom_mult = np.array(geom_mult)
283 |
284 | # Ret = namedtuple('Multiplicity', 'eigvals algebraic geometric'.split())
285 | # ret = Ret(unique_eigvals, alg_mult, geom_mult)
286 | ret = Multiplicity(unique_eigvals, alg_mult, geom_mult)
287 | return ret
288 |
289 | def b_matrix(multiplicity: Multiplicity, mult_space='min'):
290 | '''Construct the basis matrix `phi` for function approximation using eigenvalue decomposition.
291 |
292 | This function generates a matrix used to solve for function coefficients when applying matrix
293 | functions.
294 |
295 | Parameters
296 | ----------
297 | multiplicity : Multiplicity
298 | Eigenvalue multiplicity structure computed from a matrix.
299 | mult_space : 'min' | 'full'
300 | See `Multiplicity.ev_iter` for details.
301 |
302 | Returns
303 | -------
304 | np.ndarray
305 | A transformation matrix for function coefficient computation. Intended to be used like
306 | `cs = scipy.linalg.solve(b, matrix_power_series(M, len(b)))`. See also the example.
307 |
308 | Example
309 | -------
310 | >>> # Construct a random 5x5 matrix M and explicitly construct the exp(M)
311 | >>> M = np.random.randn(5,5)
312 | >>> mult = eigval_multiplicity(M)
313 | >>> b = b_matrix(mult)[:, ::-1].T
314 | >>> cs = scipy.linalg.solve(b, matrix_power_series(M, len(b)))
315 | >>>
316 | >>> # M can be set to None since `coeffs` and `eigvals` are provided
317 | >>> expM = apply_fn(None, 'exp', coeffs=cs, eigvals=mult)
318 | >>>
319 | >>> # Compare solution with reference solution from `scipy.linalg.expm`
320 | >>> expM_ref = scipy.linalg.expm(M)
321 | >>> np.linalg.norm(expM - expM_ref)
322 | >>>
323 | >>> #The expected rounding error is of order 1.11e-16 * order of biggest values * sqrt(number of calculation steps in that biggest order)
324 | np.float64(2.0755569795398968e-15)
325 | '''
326 | ret = []
327 | ev_iter = list(multiplicity.ev_iter(mult_space))
328 | dim = len(ev_iter)
329 | for (_, ev, mult, k) in ev_iter:
330 | val0 = np.concat([ev**np.arange(dim-1-k, 0, -1), [1]])
331 | val_fac = (np.arange(dim-k, 0, -1).reshape((-1, 1)) + np.arange(k).reshape((1, -1))).prod(axis=-1)
332 | val = np.concat([val0 * val_fac, [0]*k])
333 | ret.append(val)
334 | ret = np.array(ret)
335 | return ret
336 |
337 | def function_coeffs(M: np.ndarray, eigvals:np.ndarray|None|Multiplicity=None, mult_space='min', normalize_to:float|None=1.):
338 | '''Compute coefficients for matrix function computation.
339 |
340 | This function determines the necessary coefficients to compute functions applied to matrices,
341 | leveraging eigenvalue decomposition.
342 |
343 | Parameters
344 | ----------
345 | M : np.ndarray
346 | The input square matrix.
347 | eigvals : np.ndarray or Multiplicity, optional
348 | Precomputed eigenvalues or their multiplicities. If it is not of type Multiplicity then it is
349 | calculated using `eigval_multiplicity`
350 | mult_space : 'min' | 'full'
351 | See `Multiplicity.ev_iter` for details.
352 | normalize_to : float, optional, default=1.
353 | Scaling factor to normalize eigenvalues, improving numerical stability.
354 |
355 | To achieve better accuracy the `M` is rescaled such that the eigenvalues are approximately of
356 | size `normalize_to`. In particular, for a non-singular Matrix its determinate will be rescaled
357 | to `normalize_to`. Setting `normalize_to` to `None` skips the normalization step.
358 |
359 | Returns
360 | -------
361 | cs,mult : ndarray, Multiplicity
362 | The quantities necessary for applying a function to a matrix. `cs` is the `phi`-tensor. `mult`
363 | is the the multiplicity used. If the type of `eigvals` is `Multiplicity` then `mult` is equal
364 | to `eigvals`.
365 |
366 | See also
367 | --------
368 | See also `b_matrix` or `apply_fn` for an example
369 | '''
370 |
371 | # Idea behind the normalization: f(A) = f(sx) with x=A/s
372 | if isinstance(eigvals, Multiplicity):
373 | ev_mult = eigvals
374 | else:
375 | ev_mult = eigval_multiplicity(M, eigvals)
376 | if normalize_to is not None:
377 | ev_iter = list(ev_mult.ev_iter(mult_space))
378 | norm_scale = ev_mult.product_norm(mult_space)/normalize_to**(1/len(ev_iter))
379 | _ev_mult = Multiplicity(ev_mult.eigvals/norm_scale, *ev_mult[1:])
380 | _M = M/norm_scale
381 | cs_rescale = np.array([np.ones_like(norm_scale) if k==0 else norm_scale**k for _,_,_,k in ev_iter])
382 | b = b_matrix(_ev_mult, mult_space=mult_space)[:, ::-1].T
383 | _cs = scipy.linalg.solve(b, matrix_power_series(_M, len(b)))
384 | cs = cs_rescale.reshape((-1, 1, 1)) * _cs
385 | else:
386 | b = b_matrix(ev_mult, mult_space=mult_space)[:, ::-1].T
387 | cs = scipy.linalg.solve(b, matrix_power_series(M, len(b)))
388 | return cs, ev_mult
389 |
390 | def apply_fn(M: np.ndarray, f, *dfs, gen_df=None, eigvals:np.ndarray|None|Multiplicity=None, coeffs:np.ndarray|None=None, real_if_close=True, normalize_to:float|None=1., **kwargs):
391 | '''Apply a scalar function to a matrix using spectral decomposition.
392 |
393 | If `eigvals` is of type Multiplicity and `coeffs` is provided then `M` is ignored and the
394 | function is applied directly.
395 |
396 | Parameters
397 | ----------
398 | M : np.ndarray
399 | The square matrix to transform.
400 | f : function
401 | A function which is applied on the eigenvalues. Can be a predefined function like 'exp', 'log',
402 | etc. If a supported function is provided `dfs` and `gen_df` are ignored.
403 |
404 | Full list of supported predefined functions:
405 | - `exp`
406 | - `log` or `ln`
407 | - `inv`
408 | - `sin`, and `cos`
409 | - `sqrt`
410 | dfs : function
411 | Precomputed derivatives of `f`, if available.
412 | gen_df : function, optional
413 | A function that generates higher-order derivatives when needed. gen_df(k) should generate
414 | the kth derivative. Using dfs takes precedance over using gen_df. If gen_df is `None` then
415 | `numdifftools.Derivative` is used.
416 | eigvals : Optional[ndarray|Multiplicity] = None
417 | Precomputed eigenvalues or their multiplicities. If it is not of type Multiplicity then it is
418 | calculated using `eigval_multiplicity`
419 | cs : Optional[ndarray]
420 | The `phi`-tensor. See also `function_coeffs`
421 | real_if_close : bool = True
422 | Whether to `np.real_if_close` at the end.
423 | normalize_to : Optional[float] = 1.
424 | Scaling factor to normalize eigenvalues, improving numerical stability.
425 |
426 | To achieve better accuracy the `M` is rescaled such that the eigenvalues are approximately of
427 | size `normalize_to`. In particular, for a non-singular Matrix its determinate will be rescaled
428 | to `normalize_to`. Setting `normalize_to` to `None` skips the normalization step.
429 |
430 | Returns
431 | -------
432 | f_M: ndarray
433 | The matrix after applying the function `f`.
434 |
435 | Example
436 | -------
437 | >>> # Construct a random 5x5 matrix M and explicitly construct the exp(M)
438 | >>> # Compare solution with reference solution from `scipy.linalg.expm`
439 | >>> M = np.random.randn(5,5)
440 | >>> expM = apply_fn(M, 'exp')
441 | >>> expM_ref = scipy.linalg.expm(M)
442 | >>> np.linalg.norm(expM - expM_ref)
443 | >>>
444 | >>> #The expected rounding error is of order 1.11e-16 * order of biggest values * sqrt(number of calculation steps in that biggest order)
445 | np.float64(2.0755569795398968e-15)
446 |
447 | See also
448 | --------
449 | See also `b_matrix` for more fine-grained usage, which might be preferred if many functions
450 | have to be calculated for the same matrix.
451 | '''
452 | if isinstance(f, str):
453 | f = f.lower().strip()
454 | if f == 'exp':
455 | f = np.exp
456 | gen_df = lambda _: np.exp
457 | elif f in 'log ln'.split():
458 | _0 = np.array(0j)
459 | f = lambda x: np.log(x + _0)
460 | gen_df = lambda k: (lambda x: np.prod(-np.arange(1, k))/x**k)
461 | elif f == 'inv':
462 | f = lambda x: x**-1
463 | gen_df = lambda k: (lambda x: np.prod(-np.arange(1, k+1))/x**(k+1))
464 | elif f == 'sin':
465 | f = np.sin
466 | _dfs = [np.sin, np.cos, lambda x: -np.sin(x), lambda x: -np.cos(x)]
467 | gen_df = lambda k: _dfs[k%4]
468 | elif f == 'cos':
469 | f = np.cos
470 | _dfs = [np.cos, lambda x: -np.sin(x), lambda x: -np.cos(x), np.sin]
471 | gen_df = lambda k: _dfs[k%4]
472 | elif f == 'sqrt':
473 | _0 = np.array(0j)
474 | f = lambda x: np.sqrt(x + _0)
475 | gen_df = lambda k: (lambda x: np.prod(np.arange(.5, 1-k, -1))/x**(k-.5))
476 | else:
477 | raise ValueError(f'Unknown function f={f}')
478 | if coeffs is not None and isinstance(eigvals, Multiplicity):
479 | cs, ev_mult = coeffs, eigvals
480 | else:
481 | _kwargs = {k: kwargs[k] for k in 'mult_space'.split() if k in kwargs}
482 | cs, ev_mult = function_coeffs(M, eigvals, normalize_to=normalize_to, **_kwargs)
483 | f_ev_mult = ev_mult.map(f, *dfs, gen_df=gen_df, **kwargs)
484 | ret = np.tensordot(f_ev_mult, cs, ((0, ), (0, )))
485 |
486 | if real_if_close:
487 | ret = np.real_if_close(ret)
488 |
489 | return ret
490 |
--------------------------------------------------------------------------------
/matrixfuncs/utils/_misc.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | def err(x,y):
4 | '''Compute the relative error between two matrices or vectors.
5 |
6 | Parameters
7 | ----------
8 | x : np.ndarray
9 | First input array (matrix or vector).
10 | y : np.ndarray
11 | Second input array (matrix or vector) to compare against `x`.
12 |
13 | Returns
14 | -------
15 | float
16 | The relative error metric, or the absolute norm difference if both `x` and `y` have zero norm.
17 | '''
18 | nxy = np.linalg.norm(x - y)
19 | nxy_s = np.sqrt(np.linalg.norm(x) * np.linalg.norm(y))
20 | return nxy if nxy_s==0 else nxy/nxy_s
21 |
22 |
23 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "setuptools>=42",
4 | "setuptools-scm>=2.0.0"
5 | ]
6 | build-backend = "setuptools.build_meta"
7 |
8 |
9 | [tool.setuptools]
10 | packages = ["matrixfuncs"]
11 |
12 | [project]
13 | name = "matrixfuncs"
14 | dynamic = ["version"]
15 | description = "A package for computing matrix functions using eigenvalue decomposition."
16 | readme = "README.md"
17 | requires-python = ">=3.10"
18 | keywords = ["matrix", "linear algebra", "eigenvalues", "numerical methods"]
19 | license = {text = "LGPL-3.0-only"}
20 | classifiers = [
21 | "Development Status :: 4 - Beta",
22 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
23 | "Intended Audience :: Science/Research",
24 | "Programming Language :: Python :: 3",
25 | "Topic :: Scientific/Engineering :: Mathematics",
26 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
27 | ]
28 | dependencies = [
29 | "numpy",
30 | "scipy",
31 | "numdifftools"
32 | ]
33 |
34 |
35 | [project.urls]
36 | "Github" = "https://github.com/nextdorf/matrix-functions"
37 |
38 | [tool.setuptools.dynamic]
39 | version = {attr = "matrixfuncs._version.version"}
40 |
41 | [tool.setuptools_scm]
42 | write_to = "matrixfuncs/_version.py"
43 |
44 | [tool.pytest.ini_options]
45 | testpaths = ["tests"]
46 |
--------------------------------------------------------------------------------
/sinFromMFunc.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
1423 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from matrixfuncs import *
2 | from matrixfuncs.utils import *
3 | import numpy as np
4 | import pytest
5 |
6 | def direct_prod(xs0, *xss):
7 | if not hasattr(xs0, '__iter__'):
8 | xs0 = (xs0, )
9 | if len(xss) == 0:
10 | for x in xs0:
11 | yield (x,)
12 | else:
13 | inner = list(direct_prod(*xss))
14 | for x in xs0:
15 | for y in inner:
16 | yield (x,) + y
17 |
18 | def matrix_from_mult(mult: Multiplicity):
19 | t=np.arange(mult.dim)
20 | M=np.zeros(t.shape*2)
21 | M[t,t] = [ev for _,ev,_,_ in mult.ev_iter('full')]
22 | off_diag0 = [1.*(np.arange(alg) < alg-geom) for alg, geom in zip(mult.algebraic, mult.geometric)]
23 | off_diag = np.concat(off_diag0)[:-1]
24 | M[t[:-1],t[1:]] = off_diag
25 | return M
26 |
27 | def rnd_matrix_from_mult(mult: Multiplicity):
28 | M0 = matrix_from_mult(mult)
29 | Q, Q_inv = None, None
30 | while Q_inv is None:
31 | try:
32 | if np.iscomplexobj(M0):
33 | Q = np.random.randn(*M0.shape, 2) @ [1, 1j]
34 | else:
35 | Q = np.random.randn(*M0.shape)
36 | Q /= np.linalg.det(Q)
37 | Q_inv = np.linalg.inv(Q)
38 | except:
39 | continue
40 | inv_err = err(Q_inv@Q, np.eye(M0.shape[0]))
41 | if inv_err > 1e-9:
42 | pytest.warns(f'Too big error in rnd_matrix_from_mult ({inv_err}), try again...')
43 | Q_inv = None
44 | M = Q @ M0 @ Q_inv
45 | return M
46 |
47 |
48 |
--------------------------------------------------------------------------------
/tests/test_fcoeff.py:
--------------------------------------------------------------------------------
1 | from . import *
2 | import scipy
3 |
4 |
5 | frac_f_ref = lambda t: lambda A: scipy.linalg.fractional_matrix_power(A, t)
6 |
7 | @pytest.mark.parametrize('args, real_vals, dim, count', direct_prod(
8 | [
9 | dict(f='exp', f_ref=scipy.linalg.expm),
10 | dict(f='log', f_ref=scipy.linalg.logm),
11 | dict(f='sin', f_ref=scipy.linalg.sinm),
12 | dict(f='sqrt', f_ref=scipy.linalg.sqrtm),
13 | dict(f='rnd_fractional', f_ref=frac_f_ref),
14 | ],
15 | (True, False),
16 | [2, 3, 4, 5, 7, 10, 15, 20, 30],
17 | 5,
18 | ))
19 | def test_apply_fn(args, real_vals: bool, dim: int, count: int):
20 | f, f_ref = map(args.get, 'f f_ref'.split())
21 | is_rnd_fractional = f == 'rnd_fractional'
22 | for _ in range(count):
23 | if is_rnd_fractional:
24 | t = np.random.randn()*5
25 | f = lambda x: (x + 0j)**t
26 | f_ref = frac_f_ref(t)
27 | if real_vals:
28 | M = np.random.randn(dim, dim)
29 | else:
30 | M = np.random.randn(dim, dim, 2) @ [1, 1j] / 2**.5
31 | f_M = apply_fn(M, f)
32 | f_ref_M = f_ref(M)
33 | same_nan = np.isnan(f_M) == np.isnan(f_ref_M)
34 | assert np.all(same_nan), repr(np.array([M, f_M, f_ref_M]))
35 | f_M = np.nan_to_num(f_M, nan=0)
36 | f_ref_M = np.nan_to_num(f_M, nan=0)
37 | assert err(f_M, f_ref_M) < 1e-9
38 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | isolated_build = True
3 | envlist = py{313, 312, 311, 310}
4 |
5 | [testenv]
6 | deps = pytest
7 | commands = pytest {posargs}
8 |
9 |
--------------------------------------------------------------------------------
/typst/.gitignore:
--------------------------------------------------------------------------------
1 | *.pdf
2 |
--------------------------------------------------------------------------------
/typst/eq.typ:
--------------------------------------------------------------------------------
1 | // Thankfully provided by github.com/jorson
2 | // https://github.com/typst/typst/issues/380#issuecomment-1523884719
3 | // https://typst.app/project/roACTA6jaO8lqADEG5PFc-
4 |
5 |
6 | #let foldl1(a, f) = a.slice(1).fold(a.first(), f)
7 | #let concat(a) = foldl1(a, (acc, x) => acc + x)
8 | #let nonumber(e) = math.equation(block: true, numbering: none, e)
9 |
10 | #let eq(es, numberlast: false ) = if es.has("children") {
11 | let esf = es.children.filter(x => not ([ ], [#parbreak()]).contains(x))
12 | let bodyOrChildren(e) = if e.body.has("children") { concat(e.body.children) } else { e.body }
13 | let hideEquation(e) = if e.has("numbering") and e.numbering == none {
14 | nonumber(hide(e))
15 | } else [
16 | $ #hide(bodyOrChildren(e)) $ #{if e.has("label") { e.label }}
17 | ]
18 | let hidden = box(concat(
19 | if numberlast == true {
20 | esf.slice(0, esf.len()-1).map(e => nonumber(hide(e))) + (hideEquation(esf.last()),)
21 | } else if numberlast == false {
22 | esf.map(e => hideEquation(e))
23 | } else if numberlast == none {
24 | esf.map(e => nonumber(hide(e)))
25 | }))
26 | let folder(acc, e) = acc + if acc != [] { linebreak() } + e
27 | let aligned = math.equation(block: true, numbering: none, esf.fold([], folder))
28 |
29 | hidden
30 | // style(s => v(-measure(hidden, s).height, weak: true))
31 | context {
32 | v(-measure(hidden).height, weak: true)
33 | }
34 | aligned
35 | }
36 |
--------------------------------------------------------------------------------
/typst/github-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/typst/matrix-functions-docu.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": minimal-document, appendices
2 | #import "eq.typ": eq, nonumber
3 | #import "@preview/ouset:0.2.0": ouset, overset
4 | #import "@preview/wrap-it:0.1.1": wrap-content
5 | #import "@preview/codelst:2.0.2": sourcecode, sourcefile
6 |
7 | #show: minimal-document.with(
8 | title: "Documentation for Matrixfuncs",
9 | authors: (
10 | (
11 | name: "Nextdorf",
12 | // subtitle: link("https://github.com/nextdorf/matrix-functions")[matrixfuncs\@github.com],
13 | subtitle: link("https://github.com/nextdorf/matrix-functions")[
14 | #grid(columns: (auto, auto, auto), rows: 0pt, align: alignment.bottom, [matrixfuncs\@ #h(.05em)], [], [#figure(image("github-logo.svg", height: .9em))])
15 | ],
16 | ),
17 | ),
18 | // Insert your abstract after the colon, wrapped in brackets.
19 | // Example: `abstract: [This is my abstract...]`
20 | // abstract: lorem(55),
21 | keywords: ("First keyword", "Second keyword", "etc."),
22 | date: "January 28, 2025",
23 | )
24 | #set cite(style: "chicago-author-date")
25 |
26 |
27 |
28 | = Mathematical background
29 | == 2 dimensional case
30 | The Cayley-Hamilton theorem states that any square matrix $A in CC^(n times n)$ satisfies its own characteristic equation. If $p_A (lambda)=det(lambda bb(1) - A)$ is the characteristic polynomial of $A$, then substituting $A$ into it results in $p_A (A)=0$.
31 |
32 | For $n=2$:
33 |
34 | #eq[
35 | $p_A (lambda)=det(lambda bb(1)-A)=(lambda-lambda_1)(lambda-lambda_2)=lambda^2-lambda tr A+det A$
36 |
37 | $=> A^2=A tr A-bb(1)det A$
38 | ]
39 | So $A^2$ is a linear combination of $A$ and $bb(1)$. By multiplication with $A$ we find, that $A^3$ is a linear combination of $A^2$ and $A$ which reduces according to the above relation to a linear combination of $A$ and $bb(1)$. By repeatedly multiplying with $A$, we find equivalent results for $A^4$, $A^5$, ..., as well. By complete induction it follows that $A^n$ is also such a linear combination of $A$ and $bb(1)$ for any $n in NN_0$ (the base case of $n=0, 1$ is trivial):
40 |
41 | $
42 | A^n =: a_n A+b_n bb(1), quad A^(n+1)=(a_n tr A+b_n)A-(a_n det A)bb(1)
43 | $
44 | $
45 | a_(n+1) = a_n tr A+b_n, quad b_(n+1)=-a_n det A,\ quad a_0=0, quad a_1=1, quad b_0=1, quad b_1=0
46 | $
47 |
48 | In order to solve the recurrence equation we use a shift operator $Delta$ for $a_n$, s.t. $a_(n+1) = Delta a_n$
49 |
50 | $
51 | 0=(Delta^2-Delta tr A+det A)a_n=p_A (Delta)a_n=(Delta-lambda_1)(Delta-lambda_2)a_n
52 | $
53 |
54 | As an Ansatz we choose $a_n$ as a linear combination of the solutions of $0=(Delta-lambda_1)a_n$ and $0=(Delta-lambda_2)a_n$. We are motivated to pick this Ansatz because clearly any solution to $0=(Delta-lambda_(1 #[ or ] 2))a_n$ solves the above equation and the $Delta^2$ hints at a 2-dimensional solution-space. In addition the problem is linear. This approach assumes distinct eigenvalues $lambda_1, lambda_2$. When the eigenvalues coincide, additional techniques are required, which will be discussed at the end of this section and in @ch-math-general-case:
55 |
56 | $
57 | a_n = c_1 lambda_1^n +c_2 lambda_2^n
58 | $
59 | As the base case for induction, we consider the trivial cases $n=0$ and $n=1$:
60 |
61 | #eq(numberlast: true)[
62 | $ a_0 =& c_1+c_2=0, quad a_1 = c_1 lambda_1 + c_2 lambda c_2 = 1$
63 | $=> c_2 =& -c_1, quad c_1(lambda_1-lambda_2) = 1$
64 | $=> c_1 =& 1/(lambda_1-lambda_2), quad c_2 = (-1)/(lambda_1-lambda_2)$
65 | ]
66 |
67 | Thus, the explicit closed-form solutions for $a_n$ and $b_n$ are:
68 |
69 | $
70 | a_n = (lambda_1^n-lambda_2^n)/(lambda_1-lambda_2), quad b_n ouset(=,#ref()) -(lambda_2lambda_1^n - lambda_1lambda_2^n)/(lambda_1 - lambda_2)
71 | $
72 |
73 | Thus, we obtain the explicit linear combination representation of $A^n$:
74 |
75 | $
76 | A^n = (lambda_1^n-lambda_2^n)/(lambda_1-lambda_2)A -(lambda_2lambda_1^n - lambda_1lambda_2^n)/(lambda_1 - lambda_2)bb(1)
77 | $
78 |
79 | Since $A^n$ is linear in $lambda_i^n$ we know how to evaluate arbitrary polynomials of $A$. Let $q(x)$ be a polynomial:
80 |
81 | $
82 | q(A) = 1/(lambda_1-lambda_2)[(q(lambda_1) - q(lambda_2))A - (lambda_2 q(lambda_1) - lambda_1 q(lambda_2))bb(1)]
83 | $
84 |
85 | Since the coefficients only depend on $q(lambda_i)$ the formula can be generalized to all functions which are analytic in $lambda_1$ and $lambda_2$. Let $f(x)$ be an analytic function in the eigenvalues of $A$:
86 |
87 | $
88 | f(A) = 1/(lambda_1-lambda_2)[(f(lambda_1) - f(lambda_2))A - (lambda_2 f(lambda_1) - lambda_1 f(lambda_2))bb(1)]
89 | $
90 | The generalization of @func-2dim is the core of this package. The important properties are that it allows efficient computation of $f(A)$ with arbitrary precision and that the application of $f$ commutes with any linear function applied on the vector space. With other words this means there exists a rank-3 tensor $F$, which only depends on $A$, s.t. $f(A)_(i j) = F_(i j k) f(lambda_k)$.
91 |
92 | Since $f(A)$ is continuous in the eigenvalues and the set of square matrices with distinct eigenvalues is dense, we evaluate $f(A)$ for equal eigenvalues using a limiting process:
93 |
94 | #eq(numberlast: true)[
95 | $f(A | lambda_1& = lambda + epsilon, lambda_2 = lambda) = 1/epsilon [epsilon f'(lambda)A - epsilon(lambda f'(lambda) - f(lambda))bb(1)] + o(epsilon)$
96 | $ouset(-->, epsilon -> 0, &) f'(lambda)(A - lambda bb(1)) + f(lambda)bb(1)$
97 | $=> f(A) =& f'(lambda)(A - lambda bb(1)) + f(lambda)bb(1) wide #[if $lambda$ is the only eigenvalue of $A$]$
98 | ]
99 |
100 | So when $A$ is not diagonal $f(A)$ depends on $f'(lambda)$. In general we will find that $f(A)$ depends on $f(lambda_i), f'(lambda_i), ..., f^((m))(lambda_i)$ where $m$ is the difference of the algebraic and geometric multiplicity of $lambda_i$.
101 |
102 |
103 | == Applications and Examples in 2 dimensions
104 | === Comparison to Jordan form approach
105 | ...
106 | === Relation to Krylov subspaces
107 | ...
108 | === Generating $sin x$ from a difference equation
109 |
110 | Matrix functions have many applications, but the primary focus of this package is solving difference equations. Assuming you want to consider a multi-dimensional first-order linear recurrence, i.e. a sequence of vectors $(v_i)_(i=0)^infinity subset FF^n$ satisfying the recurrence relation $v_(i+1) = M v_i$ for some matrix $M in FF^(n times n)$. An obvious conclusion is $v_k = M^k v_0$. Using equations @func-2dim or @func-2dim-1eigval, we can express $M^k$ without any matrix multiplications. Instead we just need to compute $f(lambda) = lambda^k$ for all eigenvalues. As discussed in @ch-krylov, we can precompute $f(M)v_0$ or any other tensor contraction to $f(M)$ before we specify $k$, allowing for efficient evaluation for different or delayed values of $k$.
111 |
112 | Another neat application is the analytic continuation of $v_k$ in $k$ by setting $f(lambda) = e^(k ln(lambda))$, allowing for non-integer shifts. We apply this method in the example here by solving a difference equation for a sampled $sin$ function and then evaluating the numerically solved $sin$ between the sampled points.
113 |
114 | For the setup of the difference equation, consider the sum identity of $sin$:
115 |
116 | $
117 | sin(x+a) = sin(x)cos(a) + cos(x)sin(a)
118 | $
119 |
120 | For fixed $a$ $sin(x+a)$ can be expressed as a linear combination of the basis $sin x$ and $cos x$. As shown in the following derivation, $sin x$ can be written as a linear combination of $sin(x+a)$ and $sin(x+2a)$ for almost all $a$:
121 |
122 | #eq(numberlast: none)[
123 | $sin(x) =:& alpha sin(x+a) + beta sin(x+2a)$
124 | $=& alpha(sin(x)cos(a) + sin(a)cos(x)) + beta(sin(x)cos(2a) + sin(2a)cos(x))$
125 | $=& sin x(alpha cos a + beta cos(2a)) + cos x(alpha sin(a) + beta sin(2a))$
126 | $=& vec(sin x, cos x) mat(cos a, cos(2a); sin a, sin(2a)) vec(alpha, beta)$
127 | $=> vec(alpha, beta) =& mat(cos a, cos(2a); sin a, sin(2a))^(-1) vec(1, 0)$
128 | $ouset(=, #ref(), &) vec(2cos(a), -1)$
129 | ]
130 |
131 | From this we can construct a difference equation which shifts $sin x$ by $a$.
132 |
133 | #eq(numberlast: true)[
134 | $vec(sin x, sin(x-a)) =& underbrace(mat(2cos a, -1; 1, 0), =: M(a)) vec(sin(x-a), sin(x-2a))$
135 | $=> vec(sin(x+n a), sin(x+(n-1)a)) =& M(a)^n vec(sin x, sin(x-a))$
136 | $=> sin(x+n a) =& hat(e)_1 dot M(a)^n vec(sin x, sin(x-a))$
137 | $=> sin(x+y) =& hat(e)_1 dot exp(y/a ln(M(a))) vec(sin x, sin(x-a))$
138 | ]
139 |
140 | Applying the 2D recurrence formula from @func-2dim, we compute $sin(x+y)$ as follows:
141 |
142 |
143 | $
144 | sin(x+y) ouset(=, #ref()) hat(e)_1 dot 1/(lambda_1-lambda_2)&[(f(lambda_1) - f(lambda_2))M(a) - (lambda_2 f(lambda_1) - lambda_1 f(lambda_2))bb(1)] vec(sin x, sin(x-a))
145 | $
146 |
147 | #eq(numberlast: none)[
148 | $#[with ] f(lambda) =& exp(y/a ln(lambda))$
149 | $#[and ] lambda_(1,2) =& 1/2 tr M(a) plus.minus sqrt((1/2 tr M(a))^2 - det M(a))$
150 | $=& cos a plus.minus sqrt(cos^2 a - 1)$
151 | $=& e^(plus.minus i a)$
152 | $#[s.t. ] f(lambda_(1,2)) =&e^(plus.minus i y)$
153 | ]
154 | Simplifying @sin-ab-numerical yields @sin-ab.
155 |
156 |
157 | #wrap-content(
158 | figure(image("../sinFromMFunc.svg")),
159 | align: right,
160 | )[
161 | #box(height: 1em)
162 |
163 | The example code at #link("https://github.com/nextdorf/matrix-functions/blob/main/example.py")["/example.py"] illustrates how to do the above computation using the library. It generates the figure on the right:
164 |
165 | For my examples check out the snippets at #link("https://github.com/nextdorf/matrix-functions/tree/main/examples")["/examples/"] or at @ch-code-examples.
166 | ]
167 |
168 |
169 | == General case
170 |
171 | Similarly to the 2d-case we use the Cayley-Hamilton-theorem: $A in CC^(n times n), p_A (A) = 0$
172 |
173 | #eq[
174 | #nonumber($p_A (lambda) =& det(lambda bb(1) - A) = product_(k=1)^n (lambda - lambda_k) =: lambda^n - sum_(k=0)^(n-1) Lambda_k lambda^k$)
175 | $=> Lambda_k =& sum_(j_1...j_(n-k)=1,\ j_1<...
177 | $A^m =:& sum_(k=0)^(n-1) alpha_(m k) A^k$
178 | ]
179 |
180 | As before we multiply equation @matrix-power-lin-ansatz with $A$ which will generate an $A^n$ on the rhs. That generated term is substituted using @matrix-power-lin-n, resulting in a recurrence relation for $alpha_(m k)$:
181 |
182 | #eq[
183 | #nonumber($A^(m+1) =& sum_(k=1)^(n-1)(alpha_(m,k-1) + alpha_(m, n-1) Lambda_k)A^k + alpha_(m, n-1) Lambda_0 A^0$)
184 | $=> alpha_(m+1, 0) =& alpha_(m,n-1) Lambda_0, quad alpha_(m+1, k) = alpha_(m, k-1) + alpha_(m, n-1) Lambda_k$
185 | ]
186 |
187 | In order to solve the recurrence equation @recurrent-ndim, note that $alpha_(m k)$ can be expressed in terms of $alpha_(m-1, k-1)$ and $alpha_(m-1, n-1)$. $alpha_(m-1, k-1)$ can be further expressed in terms of $alpha_(m-2, k-2)$ and $alpha_(m-2, n-1)$, and so forth, until eventually $alpha_(m-k, 0)$ can be expressed in terms of $alpha_(m-k-1, n-1)$. Hence, the recurrence equation @recurrent-ndim can be solved by noticing that all $alpha_(m, n-1)$ can be reduced to a function of $alpha_(m-1, n-1), alpha_(m-2, n-1), ...$. Thus, we can simplify the recurrence relation to a recurrence relation in the first argument only and fixing the second argument to $n-1$:
188 |
189 | #eq(numberlast: true)[
190 | $alpha_(m,n-1) =& sum_(k=1)^(n-1)alpha_(m-k,n-1)Lambda_(n-k) + alpha_(m+1-n, 0)$
191 | $=& sum_(k=1)^n alpha_(m-k,n-1)Lambda_(n-k)$
192 | $=& sum_(k=0)^(n-1) alpha_(m-n+k,n-1)Lambda_k$
193 | $=> 0=& alpha_(m+n, n-1) - sum_(k=0)^(n-1) alpha_(m+k,n-1)Lambda_k$
194 | $=& (Delta^n - sum_(k=0)^(n-1) Delta^k Lambda_k) alpha_(m, n-1) wide #grid(rows: (1.25em, 0em), align: left, $#[with ] Delta alpha_(m k) = alpha_(m+1, k)$, [i.e. $Delta$ acts on the first index])$
195 | //#[with $Delta alpha_(m k) = alpha_(m+1, k)$ i.e. $Delta$ acts on the first index]$
196 | $=& p_A (Delta) alpha_(m, n-1)$
197 | $=& product_(k=1)^r (Delta - lambda_k)^(mu_k) alpha_(m, n-1)\ [with $mu_k$ being the algebraic multiplicity of $lambda_k$]$
198 | ]
199 |
200 | The general solution is $alpha_(m, n-1) = sum_(k=1)^r lambda_k^m p_k (m)$ with $p_k$ being an arbitrary polynomial of degree $mu_k-1$.
201 |
202 | Proof:
203 |
204 | #eq(numberlast: none)[
205 | $#[Induction Start: ] 0 =& (Delta - lambda)c_n => c_n =lambda c_(n-1) = lambda^n c_0$
206 | $#[Assume ] 0 =& (Delta - lambda)^m lambda^n sum_(k=0)^(m-1) c_k n^k quad forall c_0, ..., c_(m-1)$
207 | $=> (Delta - lambda)^(m+1) lambda^n sum_(k=0)^m c_k n^k =& (Delta - lambda)^m (Delta - lambda) lambda^n c_m n^m + (Delta - lambda)(Delta - lambda)^m lambda^n sum_(k=0)^(m-1) c_k n^k$
208 | $=& (Delta - lambda)^m ((n+1)^m - n^m) lambda^(n+1) c_m$
209 | $=& (Delta - lambda)^m lambda^n sum_(k=0)^(m-1) binom(m, k) lambda c_m n^k$
210 | $=& 0$
211 | $=> 0 =& (Delta - lambda)^m lambda^n sum_(k=0)^m' c_k n^k quad forall c_0, ..., c_(m'), m > m'$
212 | ]
213 |
214 | What is left to show is that the above solution is general. Considering $(Delta - lambda)^n$ as a linear operator, the solution space we are interested in is effectively the operator's kernel. Thus, the above $n$-dimensional solution is general if the dimension of the operator's kernel is $n$ as well.
215 |
216 | Now consider $overline(c)_n = (Delta - lambda) c_n => c_(n+1) = overline(c)_n + lambda c_n = sum_(k=0)^n lambda^k overline(c)_(n-k) + lambda^(n+1) c_0$
217 |
218 | Since the solution of $c_(n+1)$ is linear in $overline(c)_n$ we can consider the dimension of the solution space if $overline(c)_n$ is itself a solution of a similar equation. The solution space of $0 = (Delta - lambda_1) c_n^((1))$ is 1-dimensional. Therefore the solution space of $c_n^((m)) = (Delta - lambda_1) c_n^((m+1))$ is either of the same dimension as the solution space of $c_n^((m))$ or the dimension increases by 1. So the dimension of the solution space of $p_A (Delta) alpha_(m, n-1)$ is at most $n$. Since $sum_(k=1)^r lambda_k^m p_k(m)$ is a $sum_(k=1)^r dim(p_k) = n$ dimensional solution it is the general solution.
219 |
220 | #place(right, $qed$)
221 | \
222 | #eq[
223 | $alpha_(m, n-1) =& sum_(k=1)^r sum_(l=0)^(min(mu_k-1, m)) overline(beta)_(k l) lambda_k^(m-l) m!/((m-l)!) = sum_(k=1)^r sum_(l=0)^(min(mu_k-1, m)) overline(beta)_(k l) partial_lambda^l lambda_k^m bar_(lambda=lambda_k) =: sum_(k=1)^n beta_k lambda_k^((m))$
224 | #nonumber($beta_k =& (overline(beta)_10, ..., overline(beta)_(1,mu_1-1), overline(beta)_20, ..., overline(beta)_(2,mu_2-1), ..., overline(beta)_(r 0), ..., overline(beta)_(r,mu_r-1))_k$)
225 | #nonumber($lambda_k^((m)) =& (lambda_1^m, ..., partial_(lambda_1)^(mu_1-1) lambda_1^m, lambda_2^m, ..., partial_(lambda_2)^(mu_2-1) lambda_2^m, ..., lambda_r^m, ..., partial_(lambda_r)^(mu_r-1) lambda_r^m)_k$)
226 | ]
227 |
228 | For $m delta_(m,n-1) = beta_k lambda_k^((m))$
229 |
230 | $
231 | => arrow(beta) perp& sum_(k=1)^n hat(e)_k lambda_k^m =: arrow(lambda)^((m)) #[ for ] m = 0 ... n-2, quad #[and ] arrow(beta) dot arrow(lambda)^(n-1) = 1
232 | $
233 |
234 | // There are several ways to construct $beta$. Here we will list two ways. The first way relies on some properties of the determinant. It is the approach used in an older version of this package and is often more useful for analytical calculations. It introduces however precision-errors for numerical calculations and becomes unstable for large dimensions and/or eigenvalues.
235 |
236 | // This is why in a more recent version, the change was made to directly finding the orthogonal complement of the space spanned by $arrow(lambda)^((0)), ..., arrow(lambda)^((n-2))$. We expect the orthogonal complement to be of dimension 1, s.t. $beta$'s relation to $arrow(lambda)^((n-1))$ uniquely defines $beta$. In both cases we rely on the fact that $arrow(lambda)^((0)), ..., arrow(lambda)^((n-1))$ are linear independent.
237 |
238 | // #list([
239 | // *Construction via the determinant*
240 |
241 | If the rows (or columns) of a matrix are linearly dependent then the matrix is not invertable and its determinant will vanish. On the flip side if the entries are linear independent then the matrix's determinant will be non-zero. Thus, for a set of independent vectors ${w_(k+1), ..., w_n}$ an orthogonal tensor $T$ with
242 |
243 | #nonumber($T_(m_1...m_k) = det(hat(e)_(m_1) | ... | hat(e)_(m_k) | w_(k+1) | ... | w_n)$)
244 |
245 | can be constructed. If contracted with arbitrary vectors $(w_1, ..., w_k)$ then the orthogonal tensor will yield $det(w_1 | ... | w_n)$ which will be 0 iff the vectors $w_1, ..., w_n$ are linear dependent. This means that $T$ is non-zero, and means in particular for $k=1$ that $T$ becomes the tensor perpendicular to the vectors $w_2, ..., w_n$
246 |
247 | Since $arrow(lambda)^((0)), ..., arrow(lambda)^((n-1))$ are linear independent, we find:
248 |
249 | #math.equation(block: true, /*numbering: n => "("+[#n]+"a)"*/)[
250 | $beta_k =& det(hat(e)_k | arrow(lambda)^((n-2)) | arrow(lambda)^((n-3)) | ... | arrow(lambda)^((0))) / det(arrow(lambda)^((n-1)) | ... | arrow(lambda)^((0)))$
251 | ]
252 | // ], [
253 | // *Subtracting the span*
254 |
255 | // As elegant the above approach might be, for numerical problems it turns out to be more stable to just spanning the embedding $n$-dimensional vector space by some vectors and then subtracting the spanned space from the embedding space. The subtracted vectors will then only span the orthogonal complement:
256 |
257 | // Consider some arbitrary vectors $(v_1, v_2, ...)$ with $"span"(v_1, v_2, ...) = FF^n$, and define:
258 |
259 | // #nonumber($v'_i =& v_i - sum_(m=0)^(n-2) arrow(lambda)^((m)) (arrow(lambda)^((m)) dot v_i)/(arrow(lambda)^((m)) dot arrow(lambda)^((m)))$)
260 |
261 | // #math.equation(block: true, numbering: n => "("+[#(n - 1)]+"b)")[
262 | // $=> "span"(arrow(beta)) =& "span"(v'_1, v'_2, ...)$
263 | // ]
264 |
265 | // The condition $arrow(beta) dot arrow(lambda)^((n-1)) ouset(=,!) 1$ fixes the scale of $beta$.
266 | // ])
267 | // #counter(math.equation).update(n => n - 1)
268 |
269 | @recurrent-ndim yields for $alpha_(m k)$:
270 |
271 | $
272 | alpha_(m k) = sum_(j=1)^(k+1) alpha_(m-j, n-1) Lambda_(k+1-j) = sum_(j=1)^(k+1) arrow(beta) dot arrow(lambda)^((m-j)) Lambda_(k+1-j)
273 | $
274 |
275 | As in the two dimensional case this defines $f(A)$ if $f$ is analytic in the eigenvalues of $A$:
276 |
277 | #eq[
278 | $A^m =& sum_(k=0) A^k sum_(j=1)^(k+1) Lambda_(k+1-j) sum_(l=1)^r sum_(p=0)^(min(mu_l-1, m-j)) overline(beta)_(l p) lambda_l^(m-j-p) (m-j)!/((m-j-p)!)$
279 | #nonumber($f(A) =& sum_(k=0) A^k sum_(j=1)^(k+1) Lambda_(k+1-j) sum_(l=1)^r sum_(p=0)^(mu_l-1) overline(beta)_(l p) partial_(lambda_l)^p lambda_l^(-j) f(lambda_l)$)
280 | $=& sum_(k=0) A^k sum_(j=1)^(k+1) Lambda_(k+1-j) sum_(l=1)^r sum_(p=0)^(mu_l-1) overline(beta)_(l p) sum_(q=0)^p binom(p, q) (-1)^(p-q) (j-1+p-q)!/((j-1)!) lambda_l^(-j-p+q) f^((q))(lambda_l)$
281 | ]
282 |
283 | Since $f(A)$ is linear in $A^k$ and $f^((q))(lambda_l)$ we can define the tensors $phi$ and $phi.alt$, s.t.
284 |
285 | $
286 | f(A) = sum_(i=0)^(n-1) sum_(j=1)^r sum_(k=0)^(mu_j-1) phi_(i j)^((k)) A^i f^((k))(lambda_j), quad phi.alt_(i j k)^((l)) = sum_(m=0)^(n-1) phi_(m k)^((l))(A^m)_(i j)
287 | $
288 | #eq(numberlast: true)[
289 | $phi_(k l)^((q)) =& partial_(x_q) partial_(y_l) sum_(j=1)^(k+1) Lambda_(k+1-j) sum_(l'=1)^r sum_(p=0)^(mu_(l')-1) overline(beta)_(l' p) sum_(q'=0)^p binom(p, q') (-1)^(p-q') (j-1+p-q')!/((j-1)!) lambda_(l')^(-j-p+q') x_(q') y_(l')$
290 | $=& sum_(j=1)^(k+1) Lambda_(k+1-j) sum_(p=q)^(mu_l-1) overline(beta)_(l p) binom(p, q) (-1)^(p-q) (j-1+p-q)!/((j-1)!) lambda_l^(-j-p+q)$
291 | ]
292 |
293 | === Sanity check for $n=2$
294 |
295 | #list(
296 | [
297 | $lambda_1 != lambda_2 => mu_1 = mu_2 = 1, r = 2:$
298 |
299 | #math.equation(block: true, numbering: _ => "", [
300 | // $lambda_k^((m)) =& lambda_k^m, quad beta_(dot 0) = binom(0, 1), quad Lambda_0 = -lambda^2, quad Lambda_1 = 2lambda$\
301 | $lambda_k^((m)) =& lambda_k^m, quad beta_(dot 0) = 1/(lambda_1 - lambda_2)binom(1, -1), quad Lambda_0 = -lambda_1 lambda_2_, quad Lambda_1 = lambda_1 + lambda_2$\
302 | $phi^((0)) =& mat(Lambda_0 overline(beta)_10 slash lambda_1, Lambda_0 overline(beta)_20 slash lambda_2; Lambda_0 overline(beta)_10 slash lambda_1^2 + Lambda_1 overline(beta)_10 slash lambda_1, Lambda_0 overline(beta)_20 slash lambda_2^2 + Lambda_1 overline(beta)_20 slash lambda_2) = 1/(lambda_1 - lambda_2) mat(-lambda_2, lambda_1; 1, -1)$\
303 | $=> f(A) =& 1/(lambda_1 - lambda_2)[(f(lambda_1) - f(lambda_2))A - (lambda_2 f(lambda_1) - lambda_1 f(lambda_2))bb(1)] wide checkmark$
304 | ])
305 | ], [
306 | $lambda_1 = lambda_2 = lambda => mu_1 = 2, r = 1:$
307 |
308 | #math.equation(block: true, numbering: _ => "", [
309 | $lambda_k^((m)) =& (lambda^m, m lambda^(m-1))_k, quad beta_(dot 1) = binom(0, 1), quad Lambda_0 = -lambda^2, quad Lambda_1 = 2lambda$\
310 | $phi^((0)) =& mat(Lambda_0(overline(beta)_10 slash lambda - overline(beta)_11 slash lambda^2); Lambda_1(overline(beta)_10 slash lambda - overline(beta)_11 slash lambda^2) + Lambda_0(overline(beta)_10 slash lambda^2 - 2overline(beta)_11 slash lambda^3)) = mat(1; 0)$\
311 | $phi^((1)) =& mat(Lambda_0 overline(beta)_11 slash lambda; Lambda_1 overline(beta)_11 slash lambda + Lambda_0 overline(beta)_11 slash lambda^2) = mat(-lambda; 1)$\
312 | $=> f(A) =& f(lambda)bb(1) + f'(lambda)(A - lambda bb(1)) wide checkmark$
313 | ])
314 | ])
315 |
316 | // == Vector space properties
317 | // #let sdot(dual, vec) = $angle.l dual, vec angle.r$
318 | // Scalar product:
319 | // $
320 | // sdot(A, B) = 1/n tr(A^dagger B), quad norm(A) = sqrt(sdot(A, A))
321 | // $
322 | // ONS:
323 | // $
324 | // N_0 = bb(1), quad N_(k+1) = (A N_k - sum_(j=0)^k N_j sdot(N_j, A N_k))/norm(A N_k - sum_(j=0)^k N_j sdot(N_j, A N_k))
325 | // $
326 |
327 | = Code Examples
328 | == Example from @ch-sin-generation
329 |
330 | #sourcefile(read("../example.py"), file: "example.py",lang: "python")
331 | #figure(image("../sinFromMFunc.svg")),
332 |
333 | == More advanced Fcuntion Continution from random Samples
334 |
335 | #sourcefile(read("../examples/many_frequencies.py"), file: "example.py",lang: "python")
336 | #figure(image("../examples/many_frequencies.svg"))
337 |
338 | // #show: appendices
339 |
340 |
--------------------------------------------------------------------------------
/typst/template.typ:
--------------------------------------------------------------------------------
1 | // Based on preview/arkheion:0.1.0, https://github.com/mgoulao/arkheion/tree/main, https://typst.app/universe/package/arkheion/, MIT liensed
2 |
3 | #let minimal-document(
4 | title: "",
5 | abstract: [],
6 | keywords: (),
7 | authors: (),
8 | date: none,
9 | body,
10 | ) = {
11 | // Set the document's basic properties.
12 | set document(author: authors.map(a => a.name), title: title)
13 | set page(
14 | margin: (left: 12.5mm, right: 12.5mm, top: 12.5mm, bottom: 12.5mm),
15 | numbering: "1",
16 | number-align: center,
17 | )
18 | set text(font: "New Computer Modern", lang: "en")
19 | show math.equation: set text(weight: 400)
20 | show math.equation: set block(spacing: 0.65em)
21 | set math.equation(
22 | numbering: "(1)",
23 | supplement: none,
24 | )
25 | set heading(numbering: "1.1")
26 |
27 | let refcol = color.hsl(245deg, 80%, 45%)
28 | show link: set text(refcol)
29 | show ref: it => {
30 | set text(refcol)
31 | if it.element != none and it.element.func() == math.equation {
32 | link(it.target)[(#it)]
33 | } else {
34 | it
35 | }
36 | }
37 |
38 | // Set run-in subheadings, starting at level 4.
39 | show heading: it => {
40 | // H1 and H2
41 | if it.level == 1 {
42 | pad(
43 | top: .5em,
44 | // bottom: 1em,
45 | it
46 | )
47 | }
48 | else if it.level == 2 {
49 | pad(
50 | top: .5em,
51 | bottom: .25em,
52 | it
53 | )
54 | }
55 | else if it.level > 3 {
56 | text(11pt, weight: "bold", it.body + " ")
57 | } else {
58 | it
59 | }
60 | }
61 |
62 | line(length: 100%, stroke: 2pt)
63 | // Title row.
64 | pad(
65 | bottom: 4pt,
66 | top: 4pt,
67 | align(center)[
68 | #block(text(weight: 500, 1.75em, title))
69 | #v(1em, weak: true)
70 | ]
71 | )
72 | line(length: 100%, stroke: 2pt)
73 |
74 | // Author information.
75 | pad(
76 | top: 0.5em,
77 | x: 2em,
78 | grid(
79 | columns: (1fr,) * calc.min(3, authors.len()),
80 | gutter: 1em,
81 | ..authors.map(author => align(center)[
82 | *#author.name* \
83 | #if author.keys().contains("subtitle") {
84 | author.subtitle
85 | }
86 | ]),
87 | ),
88 | )
89 |
90 | // align(center)[#date]
91 |
92 | // Table of Contents.
93 | show outline.entry: it => {
94 | if it.level <= 2 {
95 | it = strong(it)
96 | }
97 | set text(refcol)
98 | it
99 | }
100 | outline(indent: auto)
101 |
102 | // Abstract.
103 | if abstract != [] {
104 | pad(
105 | x: 3em,
106 | top: 1em,
107 | bottom: 0.4em,
108 | align(center)[
109 | #heading(
110 | outlined: false,
111 | numbering: none,
112 | text(0.85em, smallcaps[Abstract]),
113 | )
114 | #set par(justify: true)
115 | #set text(hyphenate: false)
116 |
117 | #abstract
118 | ],
119 | )
120 | }
121 |
122 | // // Keywords
123 | // if keywords.len() > 0 {
124 | // [*_Keywords_* #h(0.3cm)] + keywords.map(str).join(" · ")
125 | // }
126 | // Main body.
127 | set par(justify: true)
128 | set text(hyphenate: false)
129 |
130 | body
131 | }
132 |
133 | #let appendices(body) = {
134 | counter(heading).update(0)
135 | counter("appendices").update(1)
136 |
137 | set heading(
138 | numbering: (..nums) => {
139 | let vals = nums.pos()
140 | let value = "ABCDEFGHIJ".at(vals.at(0) - 1)
141 | if vals.len() == 1 {
142 | return "APPENDIX " + value
143 | }
144 | else {
145 | return value + "." + nums.pos().slice(1).map(str).join(".")
146 | }
147 | }
148 | );
149 | [#pagebreak() #body]
150 | }
151 |
152 |
--------------------------------------------------------------------------------