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

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/>. 

19 

20import abc 1bca

21 

22from jax import lax 1bca

23from jax import numpy as jnp 1bca

24 

25from lsqfitgp._linalg import _pytree 1bca

26 

27 

28class SequentialOperation(_pytree.AutoPyTree, metaclass=abc.ABCMeta): 1bca

29 """See jax.lax.fori_loop for semantics.""" 

30 

31 @abc.abstractmethod 1bca

32 def __init__(self, *args): # pragma: no cover 1bca

33 pass 

34 

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 

40 

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 

45 

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`. 

50 

51 Guaranteed to be called after `init`. 

52 """ 

53 pass 

54 

55 @abc.abstractmethod 1bca

56 def iter(self, i, *inputs): # pragma: no cover 1bca

57 """Update for iteration.""" 

58 pass 

59 

60 @abc.abstractmethod 1bca

61 def finalize(self): # pragma: no cover 1bca

62 """Return final product.""" 

63 pass 

64 

65 

66def sequential_algorithm(n, ops): 1bca

67 """ 

68 Define and execute a sequential algorithm on matrices. 

69 

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. 

76 

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

89 

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

95 

96 ops = lax.fori_loop(1, n, body_fun, ops) 1ba

97 return tuple(op.finalize() for op in ops) 1ba

98 

99 

100class Producer(SequentialOperation): 1bca

101 """Produces something at each iteration but no final output.""" 

102 

103 def finalize(self): 1bca

104 pass 1ba

105 

106 

107class Consumer(SequentialOperation): 1bca

108 """Produces a final output but no iteration output.""" 

109 

110 iter_out = NotImplemented 1bca

111 

112 

113class SingleInput(SequentialOperation): 1bca

114 def __init__(self, input): # noqa: A002, `inputs` are the ops consumed 1bca

115 self.inputs = (input,) 1a

116 

117 inputs = NotImplemented 1bca

118 

119 

120class Stack(Consumer, SingleInput): 1bca

121 """`input` = an operation producing arrays.""" 

122 

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

126 

127 def iter(self, i, ai): 1bca

128 self.out = self.out.at[i, ...].set(ai) 1a

129 

130 def finalize(self): 1bca

131 """Return the stacked arrays.""" 

132 return self.out 1a

133 

134 

135class MatMulIterByFull(Consumer, SingleInput): 1bca

136 def __init__(self, input, b): # noqa: A002 1bca

137 """ 

138 Initialize the matrix multiplication. 

139 

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

151 

152 @abc.abstractmethod 1bca

153 def init(self, n, a0): # pragma: no cover 1bca

154 self.ab = ... 

155 

156 @abc.abstractmethod 1bca

157 def iter(self, i, ai): # pragma: no cover 1bca

158 self.ab = ... 

159 

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

165 

166 

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

172 

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

178 

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

182 

183 

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] 

189 

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

197 

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

203 

204 

205class Rows(Producer): 1bca

206 def __init__(self, x): 1bca

207 self.x = x 1a

208 

209 inputs = () 1bca

210 

211 def init(self, n): 1bca

212 pass 1a

213 

214 def iter_out(self, i): 1bca

215 return self.x[i, ...] 1a

216 

217 def iter(self, i): 1bca

218 pass 1a

219 

220 

221class MatMulColByRow(Consumer): 1bca

222 def __init__(self, inputa, inputb): 1bca

223 self.inputs = (inputa, inputb) 1ba

224 

225 inputs = None 1bca

226 

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

235 

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

241 

242 def finalize(self): 1bca

243 return self.ab 1ba

244 

245 

246class SumLogDiag(Consumer, SingleInput): 1bca

247 """`input` = operation producing the rows/columns of a square matrix.""" 

248 

249 def init(self, n, m0): 1bca

250 assert m0.shape == (n,) 1a

251 self.sld = jnp.log(m0[0]) 1a

252 

253 def iter(self, i, mi): 1bca

254 self.sld = self.sld + jnp.log(mi[i]) 1a

255 

256 def finalize(self): 1bca

257 """`sum(log(diag(m)))`.""" 

258 return self.sld 1a