Coverage for src/lsqfitgp/_linalg/_stdcplx.py: 44%
23 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-04 11:43 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-04 11:43 +0000
1# lsqfitgp/_linalg/_stdcplx.py
2#
3# Copyright (c) 2022, 2023, 2026, Giacomo Petrillo
4#
5# This file is part of lsqfitgp.
6#
7# lsqfitgp is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# lsqfitgp is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with lsqfitgp. If not, see <http://www.gnu.org/licenses/>.
20"""Module to estimate the time taken by standard linear algebra operations."""
22import inspect 1abc
23import timeit 1abc
25from jax import numpy as jnp 1abc
26from jax import random 1abc
27from jax.scipy import linalg as jlinalg 1abc
30def benchmark(func, *args, **kwargs): 1abc
31 timer = timeit.Timer('func(*args, **kwargs)', globals=locals())
32 n, _ = timer.autorange()
33 times = timer.repeat(5, n)
34 return min(times) / n
37# the lambdas fix the number of arguments, counted with `inspect.signature`
38ops = { 1abc
39 'chol': [
40 lambda x: jnp.linalg.cholesky(x), # noqa: PLW0108, the operation
41 lambda s: s[0] ** 3, # complexity in terms of arguments' shapes
42 ],
43 'eigh': [lambda x: jnp.linalg.eigh(x), lambda s: s[0] ** 3], # noqa: PLW0108
44 'qr-red': [
45 lambda x: jnp.linalg.qr(x, mode='reduced'),
46 lambda s: min(s) ** 2 * max(s),
47 ],
48 'qr-full': [lambda x: jnp.linalg.qr(x, mode='full'), lambda s: max(s) ** 3],
49 'svd-red': [
50 lambda x: jnp.linalg.svd(x, full_matrices=False),
51 lambda s: min(s) ** 2 * max(s),
52 ],
53 'svd-full': [
54 lambda x: jnp.linalg.svd(x, full_matrices=True),
55 lambda s: max(s) ** 3,
56 ],
57 'solve_triangular': [
58 lambda x, y: jlinalg.solve_triangular(x, y), # noqa: PLW0108
59 lambda s, t: s[0] ** 2 * t[1],
60 ],
61 'matmul': [lambda x, y: jnp.matmul(x, y), lambda s, t: s[0] * s[1] * t[1]], # noqa: PLW0108
62}
65def gen_ops_factors(n): # pragma: no cover 1abc
66 key = random.key(202208101236)
67 factors = {}
68 for op, (job, est) in ops.items():
69 print(f'{op}({n})... ', end='', flush=True) # noqa: T201
70 nparams = len(inspect.signature(job).parameters)
71 key, subkey = random.split(key)
72 m = random.normal(subkey, (nparams, n, n), jnp.float32)
73 args = m @ jnp.swapaxes(m, -2, -1)
74 time = benchmark(job, *args)
75 print(f'{time:.2g} s') # noqa: T201
76 factors[op] = time / est(*(a.shape for a in args))
77 return factors
80ops_factors = { 1abc
81 'chol': 6.03470915928483e-12,
82 'eigh': 1.824986875290051e-10,
83 'qr-red': 1.1241237493231893e-10,
84 'qr-full': 1.2058762495871633e-10,
85 'svd-red': 4.468000000342727e-10,
86 'svd-full': 4.2561762500554324e-10,
87 'solve_triangular': 4.1634716559201486e-12,
88 'matmul': 5.6301691802218555e-12,
89} # = gen_ops_factors(1000)
91ops_consts = { 1abc
92 'chol': 1.810961455339566e-06,
93 'eigh': 2.390482500195503e-06,
94 'qr-red': 2.6676162518560884e-06,
95 'qr-full': 2.6932845800183714e-06,
96 'svd-red': 3.7152979196980598e-06,
97 'svd-full': 3.663789590355009e-06,
98 'solve_triangular': 2.170706249307841e-06,
99 'matmul': 1.718031665077433e-06,
100} # = gen_ops_factors(1)
103def predtime(op, shapes, types): 1abc
104 """
105 Estimate the time taken by a linear algebra operation.
107 Parameters
108 ----------
109 op : str
110 The identifier of the operation, see `listops`.
111 shapes : sequence of tuples of integers
112 The shapes of the arguments.
113 types : sequence of numpy data types
114 The types of the arguments. They are promoted according to JAX rules.
116 Returns
117 -------
118 time : float
119 The estimated time. Not accurate. The unit of measure is seconds on a
120 particular laptop cpu used to calibrate the estimate with 1000x1000
121 matrices.
122 """
123 _, est = ops[op]
124 factor = ops_factors[op]
125 const = ops_consts[op]
126 dt = jnp.sin(jnp.empty(0, jnp.result_type(*types))).dtype
127 if dt == jnp.float64:
128 factor *= 2
129 return const + factor * est(*shapes)
132def listops(): 1abc
133 """
134 List available linear algebra operations.
136 Returns
137 -------
138 ops : dict
139 A dictionary operation identifier -> number of arguments.
140 """
141 return {op: len(inspect.signature(job).parameters) for op, (job, _) in ops.items()}