def test_constructor(): @parametric class A: def __init__(self, x, *, y=3): self.x = x self.y = y assert A.parametric assert not A.concrete assert not A.runtime_type_of with pytest.raises(RuntimeError): A.type_parameter assert A[float].parametric assert A[float].concrete assert not A[float].runtime_type_of assert A[float].type_parameter == float a1 = A[float](5.0) a2 = A(5.0) assert a1.x == 5.0 assert a2.x == 5.0 assert a1.y == 3 assert a2.y == 3 assert type_parameter(a1) == float assert type_parameter(a2) == float assert type(a1) == type(a2) assert type(a1).__name__ == type(a2).__name__ == f"A[{float}]" @parametric(runtime_type_of=True) class B: def __init__(self, x, y): self.x = x self.y = y assert B.parametric assert not B.concrete assert B.runtime_type_of with pytest.raises(RuntimeError): B.type_parameter assert B[float].parametric assert B[float].concrete assert B[float].runtime_type_of assert B[float].type_parameter == float b1 = B[float, int](5.0, 3) b2 = B(5.0, 3) assert b1.x == 5.0 assert b2.x == 5.0 assert b1.y == 3 assert b2.y == 3 assert type_parameter(b1) == (float, int) assert type_parameter(b2) == (float, int) assert type(b1) == type(b2) assert type(b1).__name__ == type(b2).__name__ == f"B[{float}, {int}]"
def test_shorthands(): model = Graph() p = GP(EQ(), graph=model) # Construct a normal distribution that serves as in input. x = p(1) assert isinstance(x, At) assert type_parameter(x) is p assert x.get() == 1 assert str(p(x)) == '{}({})'.format(str(p), str(x)) assert repr(p(x)) == '{}({})'.format(repr(p), repr(x)) # Construct a normal distribution that does not serve as an input. x = Normal(np.ones((1, 1))) with pytest.raises(RuntimeError): type_parameter(x) with pytest.raises(RuntimeError): x.get() with pytest.raises(RuntimeError): p | (x, 1) # Test shorthands for stretching and selection. p = GP(EQ(), graph=Graph()) assert str(p > 2) == str(p.stretch(2)) assert str(p[0]) == str(p.select(0))
def __init__(self, z, e, x, y, ref=None): Observations.__init__(self, x, y, ref=ref) # Extract processes. p_x, x = type_parameter(self.x), self.x.get() z = ensure_at(z, self._ref) p_z, z = type_parameter(z), z.get() # Construct the necessary kernel matrices. K_zx = self.graph.kernels[p_z, p_x](z, x) K_z = self.graph.kernels[p_z](z) # Evaluating `e.kernel(x)` will yield incorrect results if `x` is a # `MultiInput`, because `x` then still designates the particular # components of `f`. Fix that by instead designating the elements of # `e`. if isinstance(x, MultiInput): x_n = MultiInput(*(p(xi.get()) for p, xi in zip(e.kernel.ps, x.get()))) else: x_n = x # Construct the noise kernel matrix. K_n = e.kernel(x_n) # The approximation can only handle diagonal noise matrices. if not isinstance(K_n, Diagonal): raise RuntimeError('Kernel matrix of noise must be diagonal.') # And construct the components for the inducing point approximation. L_z = B.cholesky(matrix(K_z)) A = B.eye_from(K_z) + B.qf(K_n, B.transpose(B.trisolve(L_z, K_zx))) y_bar = uprank(self.y) - e.mean(x_n) - self.graph.means[p_x](x) prod_y_bar = B.trisolve(L_z, B.qf(K_n, B.transpose(K_zx), y_bar)) # Compute the optimal mean. mean = self.graph.means[p_z](z) + \ B.qf(A, B.trisolve(L_z, K_z), prod_y_bar) # Compute the ELBO. # NOTE: The calculation of `trace_part` asserts that `K_n` is diagonal. # The rest, however, is completely generic. trace_part = B.ratio( Diagonal(self.graph.kernels[p_x].elwise(x)[:, 0]) - Diagonal(B.qf_diag(K_z, K_zx)), K_n) det_part = B.logdet(2 * B.pi * K_n) + B.logdet(A) qf_part = B.qf(K_n, y_bar)[0, 0] - B.qf(A, prod_y_bar)[0, 0] elbo = -0.5 * (trace_part + det_part + qf_part) # Store relevant quantities. self.elbo = elbo self.A = A # Update observations to reflect pseudo-points. self.x = p_z(z) self.y = mean
def __init__(self, zs, e, x, y, ref=None): # Ensure `At` everywhere. zs = [ensure_at(z, ref=ref) for z in zs] # Extract graph. graph = type_parameter(zs[0]).graph # Create a representative multi-output process. p_z = graph.cross(*(type_parameter(z) for z in zs)) SparseObservations.__init__(self, p_z(MultiInput(*zs)), e, x, y, ref=ref)
def K_x(self): """Kernel matrix of the data.""" # Cache computation of the kernel matrix. if self._K_x is None: p_x, x = type_parameter(self.x), self.x.get() self._K_x = matrix(self.graph.kernels[p_x](x)) return self._K_x
def _compute(self): # Extract processes. p_x, x = type_parameter(self.x), self.x.get() p_z, z = type_parameter(self.z), self.z.get() # Construct the necessary kernel matrices. K_zx = self.graph.kernels[p_z, p_x](z, x) self._K_z = matrix(self.graph.kernels[p_z](z)) # Evaluating `e.kernel(x)` will yield incorrect results if `x` is a # `MultiInput`, because `x` then still designates the particular # components of `f`. Fix that by instead designating the elements of # `e`. if isinstance(x, MultiInput): x_n = MultiInput(*(p(xi.get()) for p, xi in zip(self.e.kernel.ps, x.get()))) else: x_n = x # Construct the noise kernel matrix. K_n = self.e.kernel(x_n) # The approximation can only handle diagonal noise matrices. if not isinstance(K_n, Diagonal): raise RuntimeError('Kernel matrix of noise must be diagonal.') # And construct the components for the inducing point approximation. L_z = B.cholesky(self._K_z) self._A = B.eye(self._K_z) + \ B.qf(K_n, B.transpose(B.trisolve(L_z, K_zx))) y_bar = uprank(self.y) - self.e.mean(x_n) - self.graph.means[p_x](x) prod_y_bar = B.trisolve(L_z, B.qf(K_n, B.transpose(K_zx), y_bar)) # Compute the optimal mean. self._mu = self.graph.means[p_z](z) + \ B.qf(self._A, B.trisolve(L_z, self._K_z), prod_y_bar) # Compute the ELBO. # NOTE: The calculation of `trace_part` asserts that `K_n` is diagonal. # The rest, however, is completely generic. trace_part = B.ratio(Diagonal(self.graph.kernels[p_x].elwise(x)[:, 0]) - Diagonal(B.qf_diag(self._K_z, K_zx)), K_n) det_part = B.logdet(2 * B.pi * K_n) + B.logdet(self._A) qf_part = B.qf(K_n, y_bar)[0, 0] - B.qf(self._A, prod_y_bar)[0, 0] self._elbo = -0.5 * (trace_part + det_part + qf_part)
def posterior_kernel(self, p_i, p_j): p_z, z = type_parameter(self.z), self.z.get() return PosteriorKernel(self.graph.kernels[p_i, p_j], self.graph.kernels[p_z, p_i], self.graph.kernels[p_z, p_j], z, self.K_z) + \ CorrectiveKernel(self.graph.kernels[p_z, p_i], self.graph.kernels[p_z, p_j], z, self.A, self.K_z)
def posterior_mean(self, p): """Get the posterior kernel of a process. Args: p (:class:`.graph.GP`): Process. Returns: :class:`.mean.Mean`: Posterior mean of `p`. """ p_x, x = type_parameter(self.x), self.x.get() return PosteriorMean(self.graph.means[p], self.graph.means[p_x], self.graph.kernels[p_x, p], x, self.K_x, self.y)
def test_shorthands(): model = Graph() p = GP(EQ(), graph=model) # Construct a normal distribution that serves as in input. x = p(1) yield assert_instance, x, At yield ok, type_parameter(x) is p yield eq, x.get(), 1 yield eq, str(p(x)), '{}({})'.format(str(p), str(x)) yield eq, repr(p(x)), '{}({})'.format(repr(p), repr(x)) # Construct a normal distribution that does not serve as an input. x = Normal(np.ones((1, 1))) yield raises, RuntimeError, lambda: type_parameter(x) yield raises, RuntimeError, lambda: x.get() yield raises, RuntimeError, lambda: p | (x, 1) # Test shorthands for stretching and selection. p = GP(EQ(), graph=Graph()) yield eq, str(p > 2), str(p.stretch(2)) yield eq, str(p[0]), str(p.select(0))
def test_parametric(): class Base1: pass class Base2: pass @parametric class A(Base1): pass assert issubclass(A, Base1) assert not issubclass(A, Base2) assert A[1] == A[1] assert A[2] == A[2] assert A[1] != A[2] a1 = A[1]() a2 = A[2]() assert type(a1) == A[1] assert type(a2) == A[2] assert isinstance(a1, A[1]) assert not isinstance(a1, A[2]) assert issubclass(type(a1), A) assert issubclass(type(a1), Base1) assert not issubclass(type(a1), Base2) # Test multiple type parameters assert A[1, 2] == A[1, 2] def tuple_elements_are_identical(tup1, tup2): if len(tup1) != len(tup2): return False for x, y in zip(tup1, tup2): if x is not y: return False return True # Test type parameter extraction. assert type_parameter(A[1]()) == 1 assert type_parameter(A["1"]()) == "1" assert type_parameter(A[1.0]()) == 1.0 assert type_parameter(A[1, 2]()) == (1, 2) assert type_parameter(A[a1]()) is a1 assert tuple_elements_are_identical(type_parameter(A[a1, a2]()), (a1, a2)) assert tuple_elements_are_identical(type_parameter(A[1, a2]()), (1, a2)) # Test that an error is raised if type parameters are specified twice. T = A[1] with pytest.raises(TypeError): T[1]
def __check_init_invariants__(self, **kwargs): T = type_parameter(self) if not (issubclass(type(T), type) and issubclass(T, GridType)): raise TypeError("Type parameter must be a subclass of GridType.") if len(kwargs) > 1 or (len(kwargs) == 1 and "periodic" not in kwargs): raise ValueError("Invalid keyword argument") periodic = kwargs.get("periodic", T is Periodic) if type(periodic) is not bool: raise TypeError("`periodic` must be a bool.") type_kw_mismatch = ( (not periodic and T is Periodic) or (periodic and issubclass(Union[T], Union[Regular, Chebyshev]))) if type_kw_mismatch: raise ValueError( "Incompatible type parameter and keyword argument")
def posterior_kernel(self, p_i, p_j): """Get the posterior kernel between two processes. Args: p_i (:class:`.graph.GP`): First process. p_j (:class:`.graph.GP`): Second process. Returns: :class:`.kernel.Kernel`: Posterior kernel between the first and second process. """ p_x, x = type_parameter(self.x), self.x.get() return PosteriorKernel(self.graph.kernels[p_i, p_j], self.graph.kernels[p_x, p_i], self.graph.kernels[p_x, p_j], x, self.K_x)
def __init__(self, *pairs, **kw_args): # Check whether there's a reference. self._ref = kw_args['ref'] if 'ref' in kw_args else None # Ensure `At` for all pairs. pairs = [(ensure_at(x, self._ref), y) for x, y in pairs] # Get the graph from the first pair. self.graph = type_parameter(pairs[0][0]).graph # Extend the graph by the Cartesian product `p` of all processes. p = self.graph.cross(*self.graph.ps) # Condition on the newly created vector-valued GP. xs, ys = zip(*pairs) self.x = p(MultiInput(*xs)) self.y = B.concat(*[uprank(y) for y in ys], axis=0)
def test(): class Base1: pass class Base2: pass @parametric class A(Base1): pass assert issubclass(A, Base1) assert not issubclass(A, Base2) assert A(1) == A(1) assert A(2) == A(2) assert A(1) != A(2) a1 = A(1)() a2 = A(2)() assert type(a1) == A(1) assert type(a2) == A(2) assert isinstance(a1, A(1)) assert not isinstance(a1, A(2)) assert issubclass(type(a1), A) assert issubclass(type(a1), Base1) assert not issubclass(type(a1), Base2) # Test multiple type parameters assert A(1, 2) == A(1, 2) # Test type parameter extraction. assert type_parameter(A(1)()) == 1 assert type_parameter(A('1')()) == '1' assert type_parameter(A(1.)()) == 1. assert type_parameter(A(1, 2)()) == (1, 2) assert type_parameter(A(a1)()) == id(a1) assert type_parameter(A(a1, a2)()) == (id(a1), id(a2)) assert type_parameter(A(1, a2)()) == (1, id(a2))
def __init__(self, x, y, ref=None): self._ref = ref self.x = ensure_at(x, self._ref) self.y = y self.graph = type_parameter(self.x).graph
def posterior_mean(self, p): p_z, z = type_parameter(self.z), self.z.get() return PosteriorMean(self.graph.means[p], self.graph.means[p_z], self.graph.kernels[p_z, p], z, self.K_z, self.mu)
def __repr__(self): T = type_parameter(self) P = "" if T is Regular else f"[{T.__name__}]" return f"Grid{P} ({' x '.join(map(str, self.shape))})"
def is_periodic(self): return type_parameter(self) is Periodic
def posterior_mean(self, p): p_x, x = type_parameter(self.x), self.x.get() return PosteriorMean(self.graph.means[p], self.graph.means[p_x], self.graph.kernels[p_x, p], x, self.K_x, self.y)
def K_x(self): """Kernel matrix of the data.""" if self._K_x is None: # Cache computation. p_x, x = type_parameter(self.x), self.x.get() self._K_x = convert(self.graph.kernels[p_x](x), AbstractMatrix) return self._K_x
def posterior_kernel(self, p_i, p_j): p_x, x = type_parameter(self.x), self.x.get() return Observations.posterior_kernel(self, p_i, p_j) + \ CorrectiveKernel(self.graph.kernels[p_x, p_i], self.graph.kernels[p_x, p_j], x, self.A, self.K_x)
def __call__(self, x, y): return self.kernels[type_parameter(x), type_parameter(y)](x.get(), y.get())
def __call__(self, x): return self.means[type_parameter(x)](x.get())
def posterior_kernel(self, p_i, p_j): p_x, x = type_parameter(self.x), self.x.get() return PosteriorKernel(self.graph.kernels[p_i, p_j], self.graph.kernels[p_x, p_i], self.graph.kernels[p_x, p_j], x, self.K_x)
def elwise(self, x, y): return self.kernels[type_parameter(x), type_parameter(y)].elwise(x.get(), y.get())