Coverage for src/lsqfitgp/_linalg/_seqalg.py: 98%
112 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/_seqalg.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/>.
20import abc 1bca
22from jax import lax 1bca
23from jax import numpy as jnp 1bca
25from lsqfitgp._linalg import _pytree 1bca
28class SequentialOperation(_pytree.AutoPyTree, metaclass=abc.ABCMeta): 1bca
29 """See jax.lax.fori_loop for semantics."""
31 @abc.abstractmethod 1bca
32 def __init__(self, *args): # pragma: no cover 1bca
33 pass
35 @property 1bca
36 @abc.abstractmethod 1bca
37 def inputs(self): # pragma: no cover 1bca
38 """Tuple of indices of other ops to be used as input."""
39 pass
41 @abc.abstractmethod 1bca
42 def init(self, n, *inputs): # pragma: no cover 1bca
43 """Initialize before the cycle starts, with the requested inputs."""
44 pass
46 @abc.abstractmethod 1bca
47 def iter_out(self, i): # pragma: no cover 1bca
48 """
49 Output passed to other ops who request it through `inputs`.
51 Guaranteed to be called after `init`.
52 """
53 pass
55 @abc.abstractmethod 1bca
56 def iter(self, i, *inputs): # pragma: no cover 1bca
57 """Update for iteration."""
58 pass
60 @abc.abstractmethod 1bca
61 def finalize(self): # pragma: no cover 1bca
62 """Return final product."""
63 pass
66def sequential_algorithm(n, ops): 1bca
67 """
68 Define and execute a sequential algorithm on matrices.
70 Parameters
71 ----------
72 n : int
73 Number of steps of the algorithm.
74 ops : list of SequentialOperation
75 Instantiated `SequentialOperation`s that represent the algorithm.
77 Return
78 ------
79 results : tuple
80 The sequence of final outputs of each operation in `ops`.
81 """
82 for i, op in enumerate(ops): 1ba
83 inputs = op.inputs 1ba
84 if any(j >= i for j in inputs): 1ba
85 msg = f'{i}-th operation {op.__class__.__name__} requested inputs {inputs!r} with forward references' 1a
86 raise ValueError(msg) 1a
87 args = (ops[j].iter_out(0) for j in inputs) 1ba
88 op.init(n, *args) 1ba
90 def body_fun(i, ops): 1ba
91 for op in ops: 1ba
92 args = (ops[j].iter_out(i) for j in op.inputs) 1ba
93 op.iter(i, *args) 1ba
94 return ops 1ba
96 ops = lax.fori_loop(1, n, body_fun, ops) 1ba
97 return tuple(op.finalize() for op in ops) 1ba
100class Producer(SequentialOperation): 1bca
101 """Produces something at each iteration but no final output."""
103 def finalize(self): 1bca
104 pass 1ba
107class Consumer(SequentialOperation): 1bca
108 """Produces a final output but no iteration output."""
110 iter_out = NotImplemented 1bca
113class SingleInput(SequentialOperation): 1bca
114 def __init__(self, input): # noqa: A002, `inputs` are the ops consumed 1bca
115 self.inputs = (input,) 1a
117 inputs = NotImplemented 1bca
120class Stack(Consumer, SingleInput): 1bca
121 """`input` = an operation producing arrays."""
123 def init(self, n, a0): 1bca
124 out = jnp.zeros((n, *a0.shape), a0.dtype) 1a
125 self.out = out.at[0, ...].set(a0) 1a
127 def iter(self, i, ai): 1bca
128 self.out = self.out.at[i, ...].set(ai) 1a
130 def finalize(self): 1bca
131 """Return the stacked arrays."""
132 return self.out 1a
135class MatMulIterByFull(Consumer, SingleInput): 1bca
136 def __init__(self, input, b): # noqa: A002 1bca
137 """
138 Initialize the matrix multiplication.
140 `input` = an operation producing pieces of left operand (a)
141 b = right operand.
142 """
143 self.inputs = (input,) 1ba
144 b = jnp.asarray(b) 1ba
145 assert b.ndim in (1, 2) 1ba
146 vec = b.ndim < 2 1ba
147 if vec: 1ba
148 b = b[:, None] 1ba
149 self.vec = vec 1ba
150 self.b = b 1ba
152 @abc.abstractmethod 1bca
153 def init(self, n, a0): # pragma: no cover 1bca
154 self.ab = ...
156 @abc.abstractmethod 1bca
157 def iter(self, i, ai): # pragma: no cover 1bca
158 self.ab = ...
160 def finalize(self): 1bca
161 ab = self.ab 1a
162 if self.vec: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true1a
163 ab = jnp.squeeze(ab, -1)
164 return ab 1a
167class MatMulRowByFull(Producer, MatMulIterByFull): 1bca
168 def init(self, n, a0): # noqa: ARG002, interface method 1bca
169 assert a0.ndim == 1 1ba
170 assert self.b.shape[0] == len(a0) 1ba
171 self.abi = a0 @ self.b 1ba
173 def iter_out(self, i): # noqa: ARG002, interface method 1bca
174 abi = self.abi 1ba
175 if self.vec: 1ba
176 abi = jnp.squeeze(abi, -1) 1ba
177 return abi 1ba
179 def iter(self, i, ai): # noqa: ARG002, interface method 1bca
180 """`i`-th row of `input @ b`."""
181 self.abi = ai @ self.b 1ba
184class SolveTriLowerColByFull(MatMulIterByFull): 1bca
185 # x[0] /= a[0, 0]
186 # for i in range(1, len(x)):
187 # x[i:] -= x[i - 1] * a[i:, i - 1]
188 # x[i] /= a[i, i]
190 def init(self, n, a0): 1bca
191 b = self.b 1a
192 del self.b 1a
193 assert a0.shape == (n,) 1a
194 assert b.shape[0] == n 1a
195 self.prevai = a0.at[0].set(0) 1a
196 self.ab = b.at[0, :].divide(a0[0]) 1a
198 def iter(self, i, ai): 1bca
199 ab = self.ab 1a
200 ab = ab - ab[i - 1, :] * self.prevai[:, None] 1a
201 self.ab = ab.at[i, :].divide(ai[i]) 1a
202 self.prevai = ai.at[i].set(0) 1a
205class Rows(Producer): 1bca
206 def __init__(self, x): 1bca
207 self.x = x 1a
209 inputs = () 1bca
211 def init(self, n): 1bca
212 pass 1a
214 def iter_out(self, i): 1bca
215 return self.x[i, ...] 1a
217 def iter(self, i): 1bca
218 pass 1a
221class MatMulColByRow(Consumer): 1bca
222 def __init__(self, inputa, inputb): 1bca
223 self.inputs = (inputa, inputb) 1ba
225 inputs = None 1bca
227 def init(self, n, a0, b0): # noqa: ARG002, interface method 1bca
228 assert a0.ndim == 1 1ba
229 assert b0.ndim <= 1 1ba
230 self.vec = b0.ndim > 0 1ba
231 if self.vec: 1ba
232 self.ab = a0[:, None] * b0[None, :] 1a
233 else:
234 self.ab = a0 * b0 1ba
236 def iter(self, i, ai, bi): # noqa: ARG002, interface method 1bca
237 if self.vec: 1ba
238 self.ab = self.ab + ai[:, None] * bi[None, :] 1a
239 else:
240 self.ab = self.ab + ai * bi 1ba
242 def finalize(self): 1bca
243 return self.ab 1ba
246class SumLogDiag(Consumer, SingleInput): 1bca
247 """`input` = operation producing the rows/columns of a square matrix."""
249 def init(self, n, m0): 1bca
250 assert m0.shape == (n,) 1a
251 self.sld = jnp.log(m0[0]) 1a
253 def iter(self, i, mi): 1bca
254 self.sld = self.sld + jnp.log(mi[i]) 1a
256 def finalize(self): 1bca
257 """`sum(log(diag(m)))`."""
258 return self.sld 1a