0
# Lazy Computation
1
2
Efficient computation with lazy evaluation for vectors and matrices that build values on-demand using custom rules and caching. This enables scalable Gaussian process operations by avoiding explicit construction of large matrices until needed.
3
4
## Capabilities
5
6
### Lazy Tensor Base
7
8
Foundation class for lazy tensors that index by object identity and provide on-demand value construction through custom building rules.
9
10
```python { .api }
11
class LazyTensor:
12
def __init__(self, rank):
13
"""
14
Initialize lazy tensor with specified rank.
15
16
Parameters:
17
- rank: Tensor rank (1 for vectors, 2 for matrices)
18
"""
19
20
def __setitem__(self, key, value):
21
"""Set value at specified key."""
22
23
def __getitem__(self, key):
24
"""Get value at specified key, building if necessary."""
25
26
def _build(self, i):
27
"""
28
Abstract method for building values on-demand.
29
30
Parameters:
31
- i: Resolved index to build value for
32
33
Returns:
34
- Built value for the index
35
"""
36
```
37
38
### Lazy Vectors
39
40
One-dimensional lazy tensors that build values using custom rules based on index sets and builder functions.
41
42
```python { .api }
43
class LazyVector(LazyTensor):
44
def __init__(self):
45
"""Initialize lazy vector."""
46
47
def add_rule(self, indices, builder):
48
"""
49
Add building rule for specified indices.
50
51
Note: For performance, indices must already be resolved!
52
53
Parameters:
54
- indices: Set of indices this rule applies to
55
- builder: Function that takes index and returns corresponding element
56
"""
57
58
def _build(self, i):
59
"""
60
Build value for index using registered rules.
61
62
Parameters:
63
- i: Index tuple to build value for
64
65
Returns:
66
- Built value
67
68
Raises:
69
- RuntimeError: If no rule can build the requested index
70
"""
71
```
72
73
### Lazy Matrices
74
75
Two-dimensional lazy tensors supporting universal rules and dimension-specific rules for efficient matrix construction.
76
77
```python { .api }
78
class LazyMatrix(LazyTensor):
79
def __init__(self):
80
"""Initialize lazy matrix."""
81
82
def add_rule(self, indices, builder):
83
"""
84
Add universal building rule for specified indices.
85
86
Note: For performance, indices must already be resolved!
87
88
Parameters:
89
- indices: Set of indices this rule applies to
90
- builder: Function taking (left_index, right_index) returning element
91
"""
92
93
def add_left_rule(self, i_left, indices, builder):
94
"""
95
Add building rule for fixed left index.
96
97
Note: For performance, indices must already be resolved!
98
99
Parameters:
100
- i_left: Fixed left index for this rule
101
- indices: Set of right indices this rule applies to
102
- builder: Function taking right_index and returning element
103
"""
104
105
def add_right_rule(self, i_right, indices, builder):
106
"""
107
Add building rule for fixed right index.
108
109
Note: For performance, indices must already be resolved!
110
111
Parameters:
112
- i_right: Fixed right index for this rule
113
- indices: Set of left indices this rule applies to
114
- builder: Function taking left_index and returning element
115
"""
116
117
def _build(self, i):
118
"""
119
Build matrix element using registered rules.
120
121
Parameters:
122
- i: (left_index, right_index) tuple
123
124
Returns:
125
- Built matrix element
126
127
Raises:
128
- RuntimeError: If no rule can build the requested element
129
"""
130
```
131
132
### Index Resolution
133
134
Internal functions for converting various key types to resolved indices used by the lazy tensor system.
135
136
```python { .api }
137
def _resolve_index(key):
138
"""Resolve key to index using object identity."""
139
140
def _resolve_index(i):
141
"""Resolve integer index directly."""
142
143
def _resolve_index(x):
144
"""Resolve tuple or sequence of keys recursively."""
145
```
146
147
## Usage Examples
148
149
### Basic Lazy Vector Usage
150
151
```python
152
import stheno
153
from stheno.lazy import LazyVector
154
155
# Create lazy vector
156
lazy_vec = LazyVector()
157
158
# Create some objects to use as indices
159
gp1 = stheno.GP(kernel=stheno.EQ(), name="gp1")
160
gp2 = stheno.GP(kernel=stheno.Matern52(), name="gp2")
161
gp3 = stheno.GP(kernel=stheno.Linear(), name="gp3")
162
163
# Add rules for building values
164
indices_123 = {id(gp1), id(gp2), id(gp3)}
165
def builder_simple(i):
166
if i == id(gp1):
167
return "Value for GP1"
168
elif i == id(gp2):
169
return "Value for GP2"
170
elif i == id(gp3):
171
return "Value for GP3"
172
else:
173
return f"Default value for {i}"
174
175
lazy_vec.add_rule(indices_123, builder_simple)
176
177
# Access values - built on demand
178
print(f"GP1 value: {lazy_vec[gp1]}")
179
print(f"GP2 value: {lazy_vec[gp2]}")
180
print(f"GP3 value: {lazy_vec[gp3]}")
181
182
# Values are cached after first access
183
print(f"GP1 value (cached): {lazy_vec[gp1]}")
184
```
185
186
### Lazy Matrix with Multiple Rule Types
187
188
```python
189
from stheno.lazy import LazyMatrix
190
import numpy as np
191
192
# Create lazy matrix
193
lazy_mat = LazyMatrix()
194
195
# Create GP objects as indices
196
gps = [stheno.GP(kernel=stheno.EQ(), name=f"gp{i}") for i in range(4)]
197
gp_ids = {id(gp) for gp in gps}
198
199
# Universal rule: diagonal elements
200
def diagonal_builder(i_left, i_right):
201
if i_left == i_right:
202
return 1.0 # Diagonal element
203
return None # Let other rules handle off-diagonal
204
205
lazy_mat.add_rule(gp_ids, diagonal_builder)
206
207
# Left rule: first GP has special relationship with others
208
def first_gp_left_rule(i_right):
209
# When first GP is on the left, return correlation
210
return 0.5
211
212
lazy_mat.add_left_rule(id(gps[0]), gp_ids, first_gp_left_rule)
213
214
# Right rule: second GP has special relationship when on right
215
def second_gp_right_rule(i_left):
216
# When second GP is on the right, return different correlation
217
return 0.3
218
219
lazy_mat.add_right_rule(id(gps[1]), gp_ids, second_gp_right_rule)
220
221
# Access matrix elements
222
print(f"Diagonal (0,0): {lazy_mat[gps[0], gps[0]]}") # Uses universal rule
223
print(f"Off-diagonal (0,1): {lazy_mat[gps[0], gps[1]]}") # Uses left rule
224
print(f"Off-diagonal (2,1): {lazy_mat[gps[2], gps[1]]}") # Uses right rule
225
print(f"Off-diagonal (2,3): {lazy_mat[gps[2], gps[3]]}") # Uses universal rule (None->0)
226
```
227
228
### Lazy Computation in GP Measures
229
230
```python
231
# Lazy computation is used internally by Stheno measures
232
measure = stheno.Measure()
233
234
# Create GPs in the measure
235
with measure:
236
gp_a = stheno.GP(kernel=stheno.EQ(), name="gp_a")
237
gp_b = stheno.GP(kernel=stheno.Matern52(), name="gp_b")
238
gp_c = gp_a + gp_b
239
measure.name(gp_c, "gp_c")
240
241
# The measure uses lazy vectors and matrices internally
242
print(f"Measure processes: {len(measure.ps)}")
243
print(f"Lazy means type: {type(measure.means)}")
244
print(f"Lazy kernels type: {type(measure.kernels)}")
245
246
# Access mean functions (computed lazily)
247
mean_a = measure.means[gp_a]
248
mean_b = measure.means[gp_b]
249
mean_c = measure.means[gp_c] # Built from gp_a + gp_b rule
250
251
print(f"Mean A: {mean_a}")
252
print(f"Mean C: {mean_c}")
253
254
# Access kernel matrix elements (computed lazily)
255
kernel_aa = measure.kernels[gp_a, gp_a]
256
kernel_ab = measure.kernels[gp_a, gp_b]
257
kernel_cc = measure.kernels[gp_c, gp_c] # Built from sum rule
258
259
print(f"Kernel (A,A): {kernel_aa}")
260
print(f"Kernel (C,C) type: {type(kernel_cc)}")
261
```
262
263
### Custom Lazy Vector for Function Caching
264
265
```python
266
# Create custom lazy vector for expensive function evaluations
267
expensive_cache = LazyVector()
268
269
def expensive_function(x):
270
"""Simulate expensive computation."""
271
print(f"Computing expensive function for {x}")
272
import time
273
time.sleep(0.1) # Simulate computation time
274
return x ** 2 + np.sin(x)
275
276
# Set up objects and their indices
277
inputs = [0.5, 1.0, 1.5, 2.0, 2.5]
278
input_ids = {id(x): x for x in inputs} # Map id back to value
279
all_ids = set(input_ids.keys())
280
281
def expensive_builder(obj_id):
282
x = input_ids[obj_id]
283
return expensive_function(x)
284
285
expensive_cache.add_rule(all_ids, expensive_builder)
286
287
# First access computes and caches
288
print("First access:")
289
result1 = expensive_cache[inputs[0]]
290
result2 = expensive_cache[inputs[1]]
291
292
print("Second access (cached):")
293
result1_cached = expensive_cache[inputs[0]] # No computation
294
result2_cached = expensive_cache[inputs[1]] # No computation
295
296
print(f"Results match: {result1 == result1_cached and result2 == result2_cached}")
297
```
298
299
### Lazy Matrix for Kernel Computations
300
301
```python
302
# Create lazy matrix for kernel evaluations
303
kernel_matrix = LazyMatrix()
304
305
# Define inputs and kernel function
306
inputs = [np.array([i]) for i in range(5)]
307
input_ids = {id(x): x for x in inputs}
308
all_ids = set(input_ids.keys())
309
310
def kernel_function(x1, x2):
311
"""RBF kernel function."""
312
return np.exp(-0.5 * np.sum((x1 - x2)**2))
313
314
def kernel_builder(id1, id2):
315
x1 = input_ids[id1]
316
x2 = input_ids[id2]
317
print(f"Computing kernel between {x1.flatten()} and {x2.flatten()}")
318
return kernel_function(x1, x2)
319
320
kernel_matrix.add_rule(all_ids, kernel_builder)
321
322
# Build kernel matrix elements on demand
323
print("Building kernel matrix:")
324
K_00 = kernel_matrix[inputs[0], inputs[0]] # Diagonal
325
K_01 = kernel_matrix[inputs[0], inputs[1]] # Off-diagonal
326
K_10 = kernel_matrix[inputs[1], inputs[0]] # Symmetric element
327
328
print(f"K(0,0) = {K_00:.3f}")
329
print(f"K(0,1) = {K_01:.3f}")
330
print(f"K(1,0) = {K_10:.3f}")
331
print(f"Symmetry check: {np.allclose(K_01, K_10)}")
332
333
# Second access uses cached values
334
print("\nAccessing cached values:")
335
K_00_cached = kernel_matrix[inputs[0], inputs[0]] # No computation
336
print(f"Cached K(0,0) = {K_00_cached:.3f}")
337
```
338
339
### Mixed Rule Priorities
340
341
```python
342
# Demonstrate rule priority in lazy matrices
343
priority_matrix = LazyMatrix()
344
345
# Create test objects
346
objs = [f"obj_{i}" for i in range(3)]
347
obj_ids = {id(obj): obj for obj in objs}
348
all_ids = set(obj_ids.keys())
349
350
# Universal rule (lowest priority)
351
def universal_rule(id1, id2):
352
return f"Universal: {obj_ids[id1]} × {obj_ids[id2]}"
353
354
priority_matrix.add_rule(all_ids, universal_rule)
355
356
# Left rule for first object (higher priority)
357
def left_rule_obj0(id2):
358
return f"Left rule: obj_0 × {obj_ids[id2]}"
359
360
priority_matrix.add_left_rule(id(objs[0]), all_ids, left_rule_obj0)
361
362
# Right rule for second object (higher priority)
363
def right_rule_obj1(id1):
364
return f"Right rule: {obj_ids[id1]} × obj_1"
365
366
priority_matrix.add_right_rule(id(objs[1]), all_ids, right_rule_obj1)
367
368
# Test rule priorities
369
print(f"(0,0): {priority_matrix[objs[0], objs[0]]}") # Left rule wins
370
print(f"(0,1): {priority_matrix[objs[0], objs[1]]}") # Left rule wins over right
371
print(f"(2,1): {priority_matrix[objs[2], objs[1]]}") # Right rule wins
372
print(f"(2,2): {priority_matrix[objs[2], objs[2]]}") # Universal rule used
373
```
374
375
### Error Handling
376
377
```python
378
# Demonstrate error handling for missing rules
379
incomplete_vector = LazyVector()
380
381
# Add rule for only some indices
382
some_objs = ["a", "b"]
383
some_ids = {id(obj) for obj in some_objs}
384
385
def partial_builder(obj_id):
386
return f"Built for {id}"
387
388
incomplete_vector.add_rule(some_ids, partial_builder)
389
390
# This works
391
result = incomplete_vector["a"]
392
print(f"Success: {result}")
393
394
# This raises RuntimeError
395
try:
396
result = incomplete_vector["c"] # Not in rule set
397
except RuntimeError as e:
398
print(f"Expected error: {e}")
399
```