Coverage for src/lsqfitgp/_gvarext/_ufunc.py: 94%
45 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/_gvarext/_ufunc.py
2#
3# Copyright (c) 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 functools 1bca
21import string 1bca
23import gvar 1bca
24import jax 1bca
25from jax import numpy as jnp 1bca
27from lsqfitgp import _signature 1bca
28from lsqfitgp._gvarext._jacobian import from_jacobian, jacobian 1bca
31def gvar_gufunc(func, *, signature=None): 1bca
32 """
33 Wrap a jax-traceable generalized ufunc with one argument to support gvars.
35 Parameters
36 ----------
37 func : callable
38 A function from one array to one array. It must be a generalized ufunc,
39 and differentiable one time with `jax`.
40 signature : str, optional
41 The signature of the generalized ufunc. If not specified, it is assumed
42 to be scalar to scalar (normal ufunc).
44 Returns
45 -------
46 decorated_func : callable
47 A function that, in addition to numerical arrays, accepts gvars and
48 returns gvars.
50 See Also
51 --------
52 numpy.vectorize
53 """
54 # parse signature
55 if signature is None: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true1bca
56 signature = '()->()'
57 sig = _signature.Signature(signature) 1bca
58 (inp,) = sig.incores 1bca
59 (out,) = sig.outcores 1bca
60 jac_sig = _signature.Signature.from_tuples([inp], [out + inp]) 1bca
62 # make jacobian function
63 deriv = jnp.vectorize(jax.jacfwd(func), signature=jac_sig.signature) 1bca
65 # get indices for summation
66 ninp = len(inp) 1bca
67 nout = len(out) 1bca
68 head_indices = '...' 1bca
69 out_indices = string.ascii_letters[:nout] 1bca
70 in_indices = string.ascii_letters[nout : nout + ninp] 1bca
71 gvar_indices = string.ascii_letters[nout + ninp] 1bca
73 # make summation formula
74 jac_indices = head_indices + out_indices + in_indices 1bca
75 in_jac_indices = head_indices + in_indices + gvar_indices 1bca
76 out_indices = head_indices + out_indices + gvar_indices 1bca
77 formula = f'{jac_indices},{in_jac_indices}->{out_indices}' 1bca
79 def gvar_function(x): 1bca
81 # unpack the gvars
82 in_mean = gvar.mean(x) 1bca
83 in_jac, indices = jacobian(x) 1bca
85 # apply function
86 out_mean = func(in_mean) 1bca
87 jac = deriv(in_mean) 1bca
89 # check shapes match
90 head_ndim = jac.ndim - nout - ninp 1bca
91 assert jac.shape[:head_ndim] == in_jac.shape[: in_jac.ndim - 1 - ninp] 1bca
93 # contract
94 out_jac = jnp.einsum(formula, jac, in_jac) 1bca
96 # pack output
97 return from_jacobian(out_mean, out_jac, indices) 1bca
99 @functools.wraps(func) 1bca
100 def decorated_func(x): 1bca
101 if isinstance(x, gvar.GVar): 1bca
102 out = gvar_function(x) 1bca
103 if not out.ndim: 103 ↛ 105line 103 didn't jump to line 105 because the condition on line 103 was always true1bca
104 out = out.item() 1bca
105 return out 1bca
106 elif getattr(x, 'dtype', None) == object: 1bca
107 return gvar_function(x) 1a
108 else:
109 return func(x) 1bca
111 return decorated_func 1bca