GEIM

Dec 13, 2024 · 5 min read
import numpy as np
import skfem  # for Finite Element Method
from skfem import Basis, ElementTriP0
from skfem import MeshTri
import matplotlib.pyplot as plt
import time
# ============================================================
# Param
# ============================================================
Nx = 50
Ny = 50

# Maillage triangulaire du carré (0,1)^2
m = MeshTri.init_tensor(
    np.linspace(0.0, 1.0, Nx + 1),
    np.linspace(0.0, 1.0, Ny + 1)
)


dx=1./Nx
dy=1./Ny

cell_areas=0.5*dx*dy

NumberOfModes = 15
NumberOfSnapshots = 20

MuMin = -2.0
MuMax = -1.0

rw = 0.15   # width of gaussian sensors

basis = Basis(m, ElementTriP0())   # P0 : one value for each element
dof_locs = basis.doflocs           # barycenters of elements for P0
ndof = dof_locs.shape[1]

# training set
mu1_train = np.linspace(MuMin, MuMax, NumberOfSnapshots)
mu2_train = np.linspace(MuMin, MuMax, NumberOfSnapshots)

# ============================================================
# Function g(x; mu1, mu2)
# ============================================================

def g_fun(x, mu1, mu2):
    return 1.0 / ((x[0] - mu1)**2 + (x[1] - mu2))

def project_g(mu1, mu2):
    return basis.project(lambda x: g_fun(x, mu1, mu2))

# ============================================================
# Gaussian kernel ell_m(x)
# ============================================================

def gaussian_weight_noC(x, xm, rw):
    """
    Gaussian centered at xm, without normalization constant.
    x: shape (2, npts)
    xm: shape (2,)
    """
    return np.exp(-0.5 * np.sum((x - xm[:, None])**2, axis=0) / rw**2)


# ============================================================
# Construction of continuous-inspired candidate sensors l_m
# ============================================================

def sigma_m_from_center(xm, rw):
    """
    Build the discrete representation of the continuous functional

        sigma_m(g) = \int_\Omega l_m(x) g dx

    for P0 functions, using barycenter quadrature:
        \int_K l_m(x) dx ~ |K| l_m(x_K)=w[K]

    Returns a weight vector w such that
        sigma_m(g_h) ~ sum_K w[K] * g_h[K]
    """
    l_vals = gaussian_weight_noC(dof_locs, xm, rw)   # l(x_K)
    w_K = cell_areas * l_vals                          # |K| * l(x_K)

    s = np.sum(w_K)
    if s > 1e-14:
        w_K = w_K / s   # normalize so that l_m(1) = 1 approximately
    return w_K

def apply_sigma_m(w, u):
    """
    Apply the functional represented by weights w to the discrete field u.
    """
    return np.dot(w, u)

# candidate sensors: one candidate centered at each barycenter
L_candidates = []
L_centers = []

for k in range(ndof):
    xm = dof_locs[:, k]
    wm = sigma_m_from_center(xm, rw)
    L_candidates.append(wm)
    L_centers.append((xm[0], xm[1]))

# ============================================================
# OFFLINE : initialization
# ============================================================

ns = NumberOfSnapshots
ntrain = ns * ns

GGrid = np.zeros((ndof, ntrain))
GinftyGrid = np.zeros(ntrain)
mu_pairs = []

col = 0
for a in mu1_train:
    for b in mu2_train:
        u = project_g(a, b)
        GGrid[:, col] = u
        GinftyGrid[col] = np.linalg.norm(u, np.inf)
        mu_pairs.append((a, b))
        col += 1

# first parameter
muIndex = np.argmax(GinftyGrid)
muIndexlist = [muIndex]

mu1_star, mu2_star = mu_pairs[muIndex]
g1 = GGrid[:, muIndex].copy()

# first magic sensor l_1: maximize |l(g1)|
scores = np.array([np.abs(apply_sigma_m(wm, g1)) for wm in L_candidates])
LIndex = np.argmax(scores)

LIndexlist = [LIndex]
Llist = [L_candidates[LIndex]]

print("First magic sensor center:", L_centers[LIndex])

# first basis function q1 normalized by l_1(q1)=1

q1 = g1 /  apply_sigma_m(Llist[0], g1)
qlist = [q1]

# ============================================================
# OFFLINE : GEIM iterations
# ============================================================

for M in range(1, NumberOfModes):
    print(f"M = {M}")

    # Interpolation matrix B_M with entries l_i(q_j)
    Qmat = np.zeros((M, M))
    for i in range(M):
        for j in range(M):
            Qmat[i, j] = apply_sigma_m(Llist[i], qlist[j])

    ResidualGrid = np.zeros((ndof, ntrain))
    ResidualInf = np.zeros(ntrain)

    # residual for each training parameter
    for col, (a, b) in enumerate(mu_pairs):
        u = GGrid[:, col].copy()

        rhs = np.array([apply_sigma_m(Llist[i], u) for i in range(M)])

        alpha = np.linalg.solve(Qmat, rhs)

        residual = u.copy()
        for i in range(M):
            residual -= alpha[i] * qlist[i]

        ResidualGrid[:, col] = residual
        ResidualInf[col] = np.linalg.norm(residual, np.inf)

    # avoid already selected parameters
    for old_idx in muIndexlist:
        ResidualInf[old_idx] = -1e30

    # new parameter
    muIndex = np.argmax(ResidualInf)
    muIndexlist.append(muIndex)

    mu1_star, mu2_star = mu_pairs[muIndex]
    rM = ResidualGrid[:, muIndex].copy()

    print("Selected parameter:", (mu1_star, mu2_star))
    print("Residual max norm :", np.max(ResidualInf))

    # choose new magic sensor maximizing |l(rM)|
    scores = np.array([abs(apply_sigma_m(w, rM)) for w in L_candidates])

    # avoid already selected sensors
    for old_L in LIndexlist:
        scores[old_L] = -np.inf

    LIndex = np.argmax(scores)
    LIndexlist.append(LIndex)
    Llist.append(L_candidates[LIndex])

    print("New magic sensor center:", L_centers[LIndex])

    # normalize new basis function by l_M(q_M)=1
    qM = rM / apply_sigma_m(Llist[-1], rM)
    qlist.append(qM)

# ============================================================
# ONLINE
# ============================================================

mu1_target = -0.5
mu2_target = -0.5

u_true = project_g(mu1_target, mu2_target)

# complete interpolation matrix
Qmat = np.zeros((NumberOfModes, NumberOfModes))
for i in range(NumberOfModes):
    for j in range(NumberOfModes):
        Qmat[i, j] = apply_sigma_m(Llist[i], qlist[j])

# rhs = [l_i(u_true)]
rhs = np.array([apply_sigma_m(Llist[i], u_true) for i in range(NumberOfModes)])

# coefficients GEIM
alpha = np.linalg.solve(Qmat, rhs)

# reconstruction GEIM
u_geim = np.zeros(ndof)
for j in range(NumberOfModes):
    u_geim += alpha[j] * qlist[j]

# ============================================================
# Test in one point
# ============================================================

xseek, yseek = 0.5, 0.5

element_finder = m.element_finder()
element_index = element_finder(np.array([xseek]), np.array([yseek]))[0]

approx_value = u_geim[element_index]
true_value_fem = u_true[element_index]
true_value_exact = g_fun(np.array([[xseek], [yseek]]), mu1_target, mu2_target)[0]

print()
print(f"Tested point : ({xseek}, {yseek})")
print(f"Approximation GEIM  : {approx_value}")
print(f"Projected FEM       : {true_value_fem}")
print(f"Exact value g(x,mu) : {true_value_exact}")
print(f"Error |GEIM - FEM|  : {abs(approx_value - true_value_fem)}")
print(f"Error |GEIM - exact|: {abs(approx_value - true_value_exact)}")
First magic sensor center: (0.01, 0.02)
M = 1
Selected parameter: (-1.0, -2.0)
Residual max norm : 0.02627306674745511
New magic sensor center: (1.0, 0.75)
M = 2
Selected parameter: (-1.631578947368421, -1.0)
Residual max norm : 0.015000046418788585
New magic sensor center: (0.01, 1.0)
M = 3
Selected parameter: (-2.0, -2.0)
Residual max norm : 0.0019474457678794344
New magic sensor center: (0.56, 0.01)
M = 4
Selected parameter: (-1.2105263157894737, -1.0)
Residual max norm : 0.00031219133964447207
New magic sensor center: (0.01, 0.46)
M = 5
Selected parameter: (-1.0, -1.368421052631579)
Residual max norm : 0.00026240204454349336
New magic sensor center: (1.0, 0.01)
M = 6
Selected parameter: (-1.3157894736842106, -1.7894736842105263)
Residual max norm : 5.765112454682722e-05
New magic sensor center: (0.43, 0.74)
M = 7
Selected parameter: (-2.0, -1.105263157894737)
Residual max norm : 1.5172047617061872e-05
New magic sensor center: (0.26, 0.01)
M = 8
Selected parameter: (-1.0, -1.6842105263157894)
Residual max norm : 6.4691190488781556e-06
New magic sensor center: (0.01, 0.7000000000000001)
M = 9
Selected parameter: (-1.1578947368421053, -1.3157894736842106)
Residual max norm : 1.9236475368019866e-06
New magic sensor center: (0.28, 0.43)
M = 10
Selected parameter: (-1.1578947368421053, -2.0)
Residual max norm : 1.5631815551238951e-06
New magic sensor center: (0.6900000000000001, 0.48)
M = 11
Selected parameter: (-1.0, -1.105263157894737)
Residual max norm : 3.9161614561813685e-07
New magic sensor center: (0.99, 1.0)
M = 12
Selected parameter: (-1.105263157894737, -1.0)
Residual max norm : 7.386577928874025e-07
New magic sensor center: (0.1, 0.21000000000000002)
M = 13
Selected parameter: (-1.631578947368421, -2.0)
Residual max norm : 1.7578529701250268e-07
New magic sensor center: (0.65, 1.0)
M = 14
Selected parameter: (-2.0, -1.5789473684210527)
Residual max norm : 1.6866644894449681e-07
New magic sensor center: (0.74, 0.01)

Tested point : (0.5, 0.5)
Approximation GEIM  : 0.498339657743457
Projected FEM       : 0.49834174836073053
Exact value g(x,mu) : 0.5
Error |GEIM - FEM|  : 2.0906172735202233e-06
Error |GEIM - exact|: 0.0016603422565429904