GMRF Precision Builders¶
The components of a latent Gaussian model, as precision operators. Each is a
quadratic form \(\tfrac{\tau}{2}\|Dx\|^2\) for a sparse difference operator \(D\),
so \(Q = \tau D^\top D\) is sparse, and each builder returns the structured
operator that sends solve, logdet, diag_inv and sampling to their
cheapest exact path:
| Builder | Returns | Exact path |
|---|---|---|
iid_precision |
lineax.DiagonalLinearOperator |
elementwise |
rw1_structure, rw2_structure, ar1_precision |
BlockTriDiag (SparseOperator when cyclic) |
block Cholesky and the block selected inverse, \(O(N d^3)\) |
besag_structure, bym2_precision |
SparseOperator |
sparse Cholesky (dense below AutoSolver's threshold until it lands) |
spde_precision (+ fem_matrices) |
SparseOperator |
sparse Cholesky, as above |
spde_precision_grid |
SpectralFunction of a KroneckerSum |
factor eigenvectors, \(O(\sum_m n_m^3)\) once and \(O(N\sum_m n_m)\) per call |
gaussx never builds graphs or meshes: a graph's structure matrix and null
space arrive as an operator and an array (kernellib's
Graph.laplacian_operator() and graph_null_space), and meshes come from
fmesher, pygmsh or meshio.
The maths.
- RW1 / RW2. Increments \(x_{i+1}-x_i\) (second differences
\(x_{i+1}-2x_i+x_{i-1}\)) are \(\mathcal N(0,\tau^{-1})\); the structure matrix
has null space \(\{\mathbf 1\}\) (\(\{\mathbf 1, t\}\)). RW2 is stored with
\(2\times 2\) blocks, so an odd \(n\) gets one decoupled unit-precision padding
node; strip it from results. Their normalising constant
\(\tfrac12\log|R|_+\) (and Besag's) is
pseudo_logdet; the padding node's eigenvalue 1 adds nothing to it. - AR(1). \(Q = \frac{\tau}{1-\rho^2}\operatorname{tridiag}(-\rho,\ 1+\rho^2,\ -\rho)\) with 1 in the corners: every marginal variance is \(1/\tau\).
- BYM2 (Riebler et al., 2016). The pair \((b, u^*)\) with
\(b = (\sqrt{1-\phi}\,v + \sqrt\phi\,u^*)/\sqrt\tau\) has a sparse joint
precision whose pattern does not depend on \((\tau, \phi)\). \(u^*\) is scaled
by
generalized_variance_scale, the geometric mean of the constrained marginal variances (Sørbye & Rue, 2014); it matches R-INLA'sinla.scale.model(golden fixture,scripts/golden/inla/). - SPDE (Lindgren, Rue & Lindström, 2011).
\((\kappa^2-\Delta)^{\alpha/2}(\tau x) = \mathcal W\) has Matérn covariance
with \(\nu = \alpha - d/2\). With P1 elements, \(K = \kappa^2\tilde C + G\)
and \(Q_\alpha = \tau^2 K(\tilde C^{-1}K)^{\alpha-1}\); on a grid with
spacing \(h\), \(Q_\alpha = \tau^2 h^d(\kappa^2 I + h^{-2}(L_1\oplus\cdots\oplus L_d))^\alpha\).
matern_spde_paramsconverts (range, \(\sigma\), \(\nu\)) into \((\kappa, \tau, \alpha)\).
Boundary effects and domain extension. The SPDE on a bounded domain,
mesh or grid, has natural (Neumann) boundary conditions. They inflate the
marginal variance within about one practical range of the boundary: up to
about \(2\sigma^2\) on an edge and \(4\sigma^2\) in a corner of a raster. Extend
the domain by at least one range beyond the region of interest and discard
the extension: a larger raster, or a mesh with an outer ring of coarser
triangles. Periodic axes of a grid (periodic=(False, True) for longitude on
a global raster) and closed surfaces such as a sphere have no boundary. On
the matching right-triangle mesh, spde_precision_grid equals
spde_precision exactly at nodes at least \(\alpha\) cells from the boundary;
nearer the boundary the mesh's lumped mass and half-weight boundary edges
differ, which is the same boundary effect.
Example.
import jax.numpy as jnp
import numpy as np
import gaussx as gx
# Temporal: a daily RW2 trend and an AR(1) nuisance
R_trend = gx.rw2_structure(364) # null space {1, t}
Q_ar = gx.ar1_precision(365, rho=0.8, tau=10.0)
sd_ar = jnp.sqrt(gx.diag_inv(Q_ar)) # block selected inverse, O(N)
# Areal: BYM2 on a graph (here the path 0 - 1 - 2 - 3; a county graph from
# kernellib in practice)
senders, receivers = np.array([1, 2, 3]), np.array([0, 1, 2])
degree = np.bincount(np.r_[senders, receivers], minlength=4).astype(float)
R = gx.besag_structure(
gx.SparseOperator.from_coo(
np.r_[np.arange(4), senders],
np.r_[np.arange(4), receivers],
jnp.asarray(np.r_[degree, -np.ones(3)]),
(4, 4),
symmetric=True,
)
)
s = gx.generalized_variance_scale(R, jnp.ones(4))
Q_bym2 = gx.bym2_precision(s * R, tau=1.5, phi=0.7) # sparse (b, u*) stack
# Continuous space: Matérn ν = 1 on a mesh, range 0.5, sd 2
vertices = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 0.5]])
triangles = np.array([[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]])
C, G = gx.fem_matrices(vertices, triangles)
kappa, tau, alpha = gx.matern_spde_params(range=0.5, sigma=2.0, nu=1.0, d=2)
Q_spde = gx.spde_precision(C, G, kappa, tau, alpha)
stations = np.array([[0.2, 0.1], [0.7, 0.6]])
A = gx.fem_projector(vertices, triangles, stations) # 3 non-zeros per row
# ...or on a global raster, with no mesh at all
Q_grid = gx.spde_precision_grid(
(90, 180), kappa=0.3, tau=1.0, alpha=2, periodic=(False, True) # wrap longitude
)
sd_grid = jnp.sqrt(gx.diag_inv(Q_grid)) # exact, two small matrix products per axis
Structured linear algebra and Gaussian primitives for JAX.
iid_precision(n: int, tau: Float[ArrayLike, '']) -> lx.DiagonalLinearOperator
¶
Precision τ I of n independent effects with variance 1/τ.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of effects. |
required |
tau
|
Float[ArrayLike, '']
|
Precision |
required |
Returns:
| Type | Description |
|---|---|
DiagonalLinearOperator
|
A |
Examples:
Source code in src/gaussx/_gmrf/_temporal.py
rw1_structure(n: int, *, spacing: Float[ArrayLike, ' n-1'] | float | None = None, cyclic: bool = False) -> BlockTriDiag | SparseOperator
¶
Structure matrix R = D₁ᵀ W D₁ of a first-order random walk.
The increments x_{i+1} − x_i ~ N(0, h_i/τ) give the precision
τ R with R = D₁ᵀ diag(1/h) D₁: the weighted path-graph Laplacian
with edge weights 1/h_i (unit weights for regular spacing). R is
singular with null space span{1}.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of nodes. |
required |
spacing
|
Float[ArrayLike, ' n-1'] | float | None
|
Gaps |
None
|
cyclic
|
bool
|
Join node |
False
|
Returns:
| Type | Description |
|---|---|
BlockTriDiag | SparseOperator
|
A positive-semidefinite |
BlockTriDiag | SparseOperator
|
symmetric |
Examples:
import jax.numpy as jnp
import gaussx
R = gaussx.rw1_structure(5)
R.mv(jnp.ones(5)) # zeros: constants are in the null space
Source code in src/gaussx/_gmrf/_temporal.py
rw2_structure(n: int, *, cyclic: bool = False) -> BlockTriDiag | SparseOperator
¶
Structure matrix R = D₂ᵀ D₂ of a second-order random walk.
The second differences x_{i+1} − 2x_i + x_{i−1} ~ N(0, 1/τ) give the
pentadiagonal precision τ R, the discrete cubic smoothing spline. Its
null space is span{1, t} (span{1} for cyclic=True).
The band is stored as a BlockTriDiag with 2 × 2 blocks, which needs
an even size: for odd n the operator has n + 1 rows, the last
one a decoupled node with unit precision. That node does not interact
with the others, so strip it from results (x[:n],
diag_inv(R)[:n]); it adds nothing to log|R| and log 1 = 0
otherwise (log τ once R is scaled by τ).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of nodes (at least 3). |
required |
cyclic
|
bool
|
Wrap the second differences around (a seasonal RW2). The
result is then a |
False
|
Returns:
| Type | Description |
|---|---|
BlockTriDiag | SparseOperator
|
A positive-semidefinite |
BlockTriDiag | SparseOperator
|
symmetric |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
import jax.numpy as jnp
import gaussx
R = gaussx.rw2_structure(6)
t = jnp.arange(6.0)
R.mv(t) # zeros: linear trends are in the null space
Source code in src/gaussx/_gmrf/_temporal.py
ar1_precision(n: int, rho: Float[ArrayLike, ''], tau: Float[ArrayLike, '']) -> BlockTriDiag
¶
Precision of a stationary AR(1) process with marginal precision τ.
x_t = rho x_{t−1} + ε_t, started from its stationary law, has
with 1 in the two corners, so every marginal variance is 1/τ
and the innovation precision is τ/(1 − rho²).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of time points (at least 2). |
required |
rho
|
Float[ArrayLike, '']
|
Lag-one correlation, |
required |
tau
|
Float[ArrayLike, '']
|
Marginal precision |
required |
Returns:
| Type | Description |
|---|---|
BlockTriDiag
|
A positive-definite |
Examples:
import jax.numpy as jnp
import gaussx
Q = gaussx.ar1_precision(50, rho=0.8, tau=10.0)
gaussx.diag_inv(Q) # all 0.1: the marginal variance 1/τ
Source code in src/gaussx/_gmrf/_temporal.py
besag_structure(laplacian_op: lx.AbstractLinearOperator) -> lx.AbstractLinearOperator
¶
Validate a graph Laplacian as a Besag (ICAR) structure matrix.
Checks that the operator is square and symmetric and, when its values
are concrete, that its rows sum to zero (constants are in its null
space), then tags it positive semidefinite. A SparseOperator or
BlockTriDiag keeps its type (and so its sparse dispatch); any other
operator is wrapped in a lineax.TaggedLinearOperator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
laplacian_op
|
AbstractLinearOperator
|
The weighted graph Laplacian |
required |
Returns:
| Type | Description |
|---|---|
AbstractLinearOperator
|
The same matrix, tagged symmetric and positive semidefinite. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If it is not square or symmetric, or a row sum is not zero. |
Examples:
import jax.numpy as jnp
import numpy as np
import gaussx
# Path graph 0 - 1 - 2, each edge once
R = gaussx.SparseOperator.from_coo(
np.array([0, 1, 2, 1, 2]),
np.array([0, 1, 2, 0, 1]),
jnp.array([1.0, 2.0, 1.0, -1.0, -1.0]),
(3, 3),
symmetric=True,
)
R = gaussx.besag_structure(R) # now tagged positive semidefinite
Source code in src/gaussx/_gmrf/_areal.py
generalized_variance_scale(structure: lx.AbstractLinearOperator, null_space: Float[ArrayLike, 'n k'] | Float[ArrayLike, ' n'], *, eps: float | None = None) -> Float[Array, '']
¶
Generalized variance of an intrinsic GMRF (Sørbye & Rue, 2014).
The geometric mean of the marginal variances under the constraint
Vᵀx = 0 (V the null space),
so that s · R has generalized variance one: the scaling that makes a
precision τ mean the same thing for every graph (BYM2, scaled
RW1 / RW2).
Two exact paths:
- Eigen-structured
R(aKroneckerSumgrid Laplacian, aDiagonalisedOperatoror aSpectralFunction): the diagonal of the pseudo-inverse from the factor eigenvectors (gaussx.diag_invwithpinv=True). This assumesnull_spacespans exactly the zero eigenspace. - Anything else (a
SparseOperatorgraph Laplacian, aBlockTriDiagrandom walk): as R-INLA'sinla.scale.model, the marginal variances ofR + εIfromgaussx.diag_inv(the block selected inverse for aBlockTriDiag; sparse Cholesky / Takahashi for aSparseOperatoras that dispatch lands), then the kriging correction for the constraint,Σ = S − S V (Vᵀ S V)⁻¹ Vᵀ SwithS = (R + εI)⁻¹, which needsksolves.
For a disconnected graph scale each connected component separately.
structure may have one more row than null_space: the decoupled
padding node of an odd-size gaussx.rw2_structure, which is then
left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure
|
AbstractLinearOperator
|
The structure matrix |
required |
null_space
|
Float[ArrayLike, 'n k'] | Float[ArrayLike, ' n']
|
Basis of its null space, shape |
required |
eps
|
float | None
|
Ridge |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
The scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the sizes disagree. |
Examples:
import jax.numpy as jnp
import gaussx
R = gaussx.rw1_structure(20)
s = gaussx.generalized_variance_scale(R, jnp.ones(20))
# R_scaled = s * R has generalized variance 1
Source code in src/gaussx/_gmrf/_areal.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |
bym2_precision(structure_scaled: lx.AbstractLinearOperator, tau: Float[ArrayLike, ''], phi: Float[ArrayLike, '']) -> SparseOperator
¶
Joint precision of the BYM2 pair (b, u*) (Riebler et al., 2016).
b = (√(1−φ) v + √φ u*)/√τ with v ~ N(0, I) and u* the scaled
ICAR field (structure R*) gives
and the marginal covariance of b is
((1−φ)I + φ R*⁺)/τ under u*'s sum-to-zero constraint. The
pattern (R*'s, shifted, plus two diagonals) is built on the host
once per pattern of R* and does not depend on (τ, φ).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure_scaled
|
AbstractLinearOperator
|
The scaled structure |
required |
tau
|
Float[ArrayLike, '']
|
Precision |
required |
phi
|
Float[ArrayLike, '']
|
Mixing |
required |
Returns:
| Type | Description |
|---|---|
SparseOperator
|
A symmetric positive-semidefinite |
SparseOperator
|
acting on the stacked vector |
SparseOperator
|
null space, which the sum-to-zero constraint removes. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Examples:
import jax.numpy as jnp
import numpy as np
import gaussx
R = gaussx.SparseOperator.from_coo(
np.array([0, 1, 2, 1, 2]),
np.array([0, 1, 2, 0, 1]),
jnp.array([1.0, 2.0, 1.0, -1.0, -1.0]),
(3, 3),
symmetric=True,
)
s = gaussx.generalized_variance_scale(R, jnp.ones(3))
Q = gaussx.bym2_precision(s * R, tau=1.5, phi=0.7) # (6, 6)
Source code in src/gaussx/_gmrf/_areal.py
spde_precision(C_lumped: lx.DiagonalLinearOperator, G: SparseOperator, kappa: Float[ArrayLike, ''], tau: Float[ArrayLike, ''], alpha: int) -> SparseOperator
¶
SPDE precision Q_α = τ² K (C̃⁻¹K)^{α−1} with K = κ²C̃ + G.
The pattern of Q_α (the (α−1)-ring neighbourhood of G's) and
the index triples of each sparse product are computed once on the host
per pattern and cached; only the values depend on κ and τ, so
jit, grad and vmap over them never redo the symbolic work.
The result goes to sparse Cholesky through gaussx.solve,
gaussx.logdet and gaussx.diag_inv as those dispatch for a
SparseOperator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
C_lumped
|
DiagonalLinearOperator
|
Lumped (diagonal) mass matrix |
required |
G
|
SparseOperator
|
Stiffness matrix, from |
required |
kappa
|
Float[ArrayLike, '']
|
Inverse range parameter |
required |
tau
|
Float[ArrayLike, '']
|
Scale |
required |
alpha
|
int
|
Integer order |
required |
Returns:
| Type | Description |
|---|---|
SparseOperator
|
A symmetric positive-definite |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
import numpy as np
import gaussx
# Two triangles forming the unit square
vertices = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
triangles = np.array([[0, 1, 2], [0, 2, 3]])
C, G = gaussx.fem_matrices(vertices, triangles)
kappa, tau, alpha = gaussx.matern_spde_params(
range=0.5, sigma=1.0, nu=1.0, d=2
)
Q = gaussx.spde_precision(C, G, kappa, tau, alpha) # (4, 4), alpha = 2
Source code in src/gaussx/_gmrf/_spde.py
spde_precision_grid(shape: tuple[int, ...], kappa: Float[ArrayLike, ''], tau: Float[ArrayLike, ''], alpha: int, *, spacing: float = 1.0, periodic: bool | tuple[bool, ...] = False) -> SpectralFunction
¶
SPDE precision on a regular grid: a function of a Kronecker sum.
With spacing h the right-triangle mesh has C̃ = h^d I and
G = h^{d−2}(L_1 ⊕ … ⊕ L_d), the (2d+1)-point Laplacian, so
which is τ²h²(κ² + λ/h²)^α on the eigenvalues λ of the Kronecker
sum for a raster (d = 2). Each L_m is the path-graph Laplacian
(natural boundary) or, on a periodic axis, the cycle-graph Laplacian.
Its eigendecomposition is computed once on the host, so gaussx.solve,
gaussx.logdet, gaussx.diag_inv and exact sampling
(SpectralFunction.sqrt_matmul with inverse=True) cost
O(Σ_m n_m³) once and O(N Σ_m n_m) per call, with no mesh and no
Cholesky. In the interior it equals spde_precision on the matching
right-triangle mesh; at a non-periodic boundary the mesh's lumped mass
and half-weight boundary edges differ (see the module notes on
boundary effects and domain extension).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shape
|
tuple[int, ...]
|
Grid shape |
required |
kappa
|
Float[ArrayLike, '']
|
Inverse range |
required |
tau
|
Float[ArrayLike, '']
|
Scale |
required |
alpha
|
int
|
Integer order |
required |
spacing
|
float
|
Grid spacing |
1.0
|
periodic
|
bool | tuple[bool, ...]
|
Wrap all axes ( |
False
|
Returns:
| Type | Description |
|---|---|
SpectralFunction
|
A positive-definite |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
import gaussx
# Matérn nu = 1 on a 30 x 60 raster, wrapping the second axis
Q = gaussx.spde_precision_grid(
(30, 60), kappa=0.3, tau=1.0, alpha=2, periodic=(False, True)
)
sd = gaussx.diag_inv(Q) ** 0.5 # exact, through the factor eigenvectors
Source code in src/gaussx/_gmrf/_spde.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | |
matern_spde_params(range: Float[ArrayLike, ''], sigma: Float[ArrayLike, ''], nu: float, d: int) -> tuple[Array, Array, int]
¶
SPDE parameters (κ, τ, α) of a Matérn field.
κ = √(8 nu)/rho for the practical range rho (correlation ≈ 0.13 at
distance rho), α = nu + d/2, and τ from the marginal variance
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
range
|
Float[ArrayLike, '']
|
Practical range |
required |
sigma
|
Float[ArrayLike, '']
|
Marginal standard deviation |
required |
nu
|
float
|
Smoothness |
required |
d
|
int
|
Spatial dimension, concrete. |
required |
Returns:
| Type | Description |
|---|---|
tuple[Array, Array, int]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
import gaussx
kappa, tau, alpha = gaussx.matern_spde_params(
range=50.0, sigma=2.0, nu=1.0, d=2
) # alpha == 2
Source code in src/gaussx/_gmrf/_spde.py
fem_matrices(vertices: Float[ArrayLike, 'V D'], triangles: Int[ArrayLike, 'T 3']) -> tuple[lx.DiagonalLinearOperator, SparseOperator]
¶
Lumped mass C̃ and stiffness G of P1 elements on a triangle mesh.
The local matrices (module docstring) are computed for all triangles at
once with einx and scattered with a segment_sum into G's
pattern, which is built on the host from triangles (two vertices are
coupled iff they share a triangle). vertices may be traced; only
triangles must be concrete.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
Float[ArrayLike, 'V D']
|
Vertex coordinates, shape |
required |
triangles
|
Int[ArrayLike, 'T 3']
|
Vertex indices of each triangle, shape |
required |
Returns:
| Type | Description |
|---|---|
DiagonalLinearOperator
|
|
SparseOperator
|
|
tuple[DiagonalLinearOperator, SparseOperator]
|
symmetric positive-semidefinite |
tuple[DiagonalLinearOperator, SparseOperator]
|
the constants on each connected component). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the shapes are wrong or an index is out of range. |
Examples:
import numpy as np
import gaussx
# The unit right triangle: |T| = 1/2
vertices = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]])
C, G = gaussx.fem_matrices(vertices, np.array([[0, 1, 2]]))
C.as_matrix() # I / 6
G.as_matrix() # [[1, -1/2, -1/2], [-1/2, 1/2, 0], [-1/2, 0, 1/2]]
Source code in src/gaussx/_gmrf/_fem.py
fem_projector(vertices: Float[ArrayLike, 'V D'], triangles: Int[ArrayLike, 'T 3'], points: Float[ArrayLike, 'n D'], *, triangle_index: Int[ArrayLike, ' n'] | None = None) -> SparseOperator
¶
Observation matrix A of P1 interpolation: (A w)_k = Σ_i ψ_i(s_k) w_i.
Row k holds the three barycentric weights of point s_k in its
triangle, so A has three non-zeros per row and, in a Laplace
Hessian Q + AᵀWA, never couples vertices that are not already
neighbours.
Point location (on the host, blocked brute force over all triangles):
- Planar meshes (
V × 2): the barycentric test; a point outside every triangle raises. - Surface meshes (
V × 3) that are star-shaped about the centroid of their vertices (spheres, icospheres; every triangle must face away from the centroid, which also requires a consistent orientation): the ray from the centroid through each point is intersected with the triangles, and the weights are taken at the intersection, i.e. the point is projected radially onto the mesh (its distance to the mesh, the chord error, is ignored). - Any other surface mesh needs
triangle_index.
With triangle_index no location is done and the weights are those of
the point's orthogonal projection onto the triangle's plane; points
may then be traced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
Float[ArrayLike, 'V D']
|
Vertex coordinates, shape |
required |
triangles
|
Int[ArrayLike, 'T 3']
|
Vertex indices of each triangle, shape |
required |
points
|
Float[ArrayLike, 'n D']
|
Observation locations, shape |
required |
triangle_index
|
Int[ArrayLike, ' n'] | None
|
The triangle containing each point, shape |
None
|
Returns:
| Type | Description |
|---|---|
SparseOperator
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a point is not on the mesh, or a surface mesh is not
star-shaped about its centroid and |
Examples:
import numpy as np
import gaussx
vertices = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
triangles = np.array([[0, 1, 2], [0, 2, 3]])
A = gaussx.fem_projector(vertices, triangles, np.array([[0.5, 0.25]]))
A.as_matrix() # [[0.5, 0.25, 0.25, 0]]: barycentric weights
Source code in src/gaussx/_gmrf/_fem.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | |