Dec 13, 2024 · 5 min read

The Empiral Interpolation Method (EIM)

Script by Elise Grosjean (ENSTA-Paris, elise.grosjean@ensta-paris.fr)

import sys
!{sys.executable} -m pip install numpy
!{sys.executable} -m pip install matplotlib
!{sys.executable} -m pip install scikit-fem

The EIM build a linear combination of fully determined solutions from basis functions $(q_i)_{i=1,...,M}$ depending on some interpolating points (called “magic points”) and some values $(\alpha_i)_{i=1,...,M}$ relying on certain instances of the parameter $\nu$, selected within the algorithm.

$$ g(x,\nu)= \frac1{\sqrt{(x_1-\nu_1)^2+(y_1-\nu_2)}},$$

with $x=(x_1,x_2)$ and $\nu=(\nu_1,\nu_2)$.

  • “OFFLINE”

The first chosen parameter $\nu^1$ is the one that maximizes $g$ on norm $\mathcal{L}^{\infty}$ (we want to retrieve most information on $g$ with few points) and the associated magic point $x^1$ is the point that gives the most information on $g(\cdot,\nu^1)$ i.e. which maximizes its modulus.

Then the first basis function is $q_1(\cdot) = \frac{g(\cdot,\nu^1)}{g(x^1, \nu^1)}$. We can then find $\alpha_1$ as the coefficient corresponding to this basis function: We compute for each training parameters $\nu \in \mathcal{G}$ the real $G$ such that $G=g(x^1,\nu)$ and then we solve the problem $Q \alpha^1(\nu)=G$ where $Q$ at this initialization step is just $q_1(x^1)$.

$$ \forall 1 \leq i \leq M-1,\ \mathcal{I}_{M-1}[g](x^i)= g(x^i),$$$$\mathcal{I}_{M-1}[g]=\sum_{j=1}^{M-1} \alpha_j^{M-1} q_j.$$
  • " ONLINE "

Now we are interested by a new parameter $\nu^{target}$. We compute for $\nu^{target} \in \mathcal{G}$ the vector $G \in \mathbb{R}^M$ such that $G_i=g(x^i,\nu^{target})$ for $i=1,\dots,M$. Then we solve the problem $Q \alpha(\nu^{target})=G$ where $Q \in \mathbb{R}^{M\times M}$ is such that $Q_{i,j}=q_j(x^i)$ for $i,j=1,\dots,M$ (where $q_j$ are our basis functions and $x^i$ our magic points. The solution approximation is then given by $ g^M(x,\nu^{target})=\sum_{i=1}^M \alpha(\nu^{target}) q(x). $

# import packages
import skfem  # for Finite Element Method
import numpy as np
import matplotlib.pyplot as plt
""" First we define a mesh for the unit square with the boundary decomposition """
mesh= skfem.MeshTri().refined(4).with_boundaries(
        {                                                                                                                                
            "left": lambda x: x[0] == 0,                                                                                            
            "right": lambda x: x[0] == 1,            
            "down": lambda x: x[1] == 0,                                                                                            
            "up": lambda x: x[1] == 1,     
            
        }                                                                                                                               
)

print(mesh)
mesh
<skfem MeshTri1 object>
  Number of elements: 512
  Number of vertices: 289
  Number of nodes: 289
  Named boundaries [# facets]: left [16], bottom [16], right [16], top [16], down [16], up [16]

svg

from skfem import *

""" OFFLINE """
""" Initialization: M=1 ! """
# We seek mu* and x* s.t mu*=argmax_mu ||g(mu)||_inf and x*=argmax_x ||g(mu*)||_inf

NumberOfModes=15;#number of basis functions (a priori given)
NumberOfSnapshots=20;#training parameters number (ns*ns)

#real mu1,mu2;//parameter in [-1,-0.01]^2
g = lambda x: 1/np.sqrt((x[0]-mu[0])**2+(x[1]-mu[1])) #exact non-linear (wrt parameters) function

basis = Basis(mesh, ElementTriP0())  # here we consider P0 finite elements

#grid parameter repartition
MuBegin = -1.
MuEnd = -0.01

mu1=np.zeros(NumberOfSnapshots)
mu2=np.zeros(NumberOfSnapshots)
for i in range(NumberOfSnapshots):
    mu1[i]=MuBegin+i*np.abs(MuBegin-MuEnd)/(NumberOfSnapshots-1)
mu2=mu1.copy()

muGrid=np.meshgrid(mu1,mu2) #grid of parameters

GGrid=np.zeros((basis.doflocs.shape[1],NumberOfSnapshots*NumberOfSnapshots)) #function g evaluated for all degrees of freedom (doflocs) 
GinftyGrid=np.zeros(NumberOfSnapshots*NumberOfSnapshots) # L^infty norm of g

for i,elem1 in enumerate((list(mu1))):
    for j,elem2 in enumerate((list(mu2))):
        mu=[elem1,elem2] #update mu
        u=basis.project(g) #u is simply the projection onto the FEM basis of g with the parameter mu
        
        GGrid[:,i*NumberOfSnapshots+j]=u #g(mu)
        GinftyGrid[i*NumberOfSnapshots+j]=np.linalg.norm(u,np.inf) # norm_inf g(mu)

muIndex=np.argmax(GinftyGrid) 
muIndexlist=[muIndex]
mu1Index=int(np.floor(muIndex/NumberOfSnapshots))
mu2Index=int(muIndex-mu1Index*NumberOfSnapshots)

mu=[mu1[mu1Index],mu2[mu2Index]]
g1=basis.project(g) # find the mu* s.t argmax_mu ||g(mu)||_inf
XIndex=np.argmax(g1)

dof_locs = basis.doflocs
MagicPoint=[dof_locs[0][XIndex],dof_locs[1][XIndex]] #Find first magic point
print(" First magic point ", MagicPoint)

q1= g1/g1[XIndex] # First basis function !
qlist=[q1]

MagicPointlist=[MagicPoint]
MagicPointIndiceslist=[XIndex]
 First magic point  [0.03125, 0.03125]
$$ g(x^i,\nu)=\sum_{i=0}^{M-1} \alpha^j(\nu) q^j(x^i)$$

where $(q^j)_{j=0,\dots,M-1}$ are the basis functions already computed.

So for all parameters $\nu$, we find $\alpha(\nu)$ such that $Q \ \alpha = G$, where $Q_{i,j}=q^j(x^i)$ for $i,j=1,\dots,M$ and $G= g(x^i,\nu)$

### EIM offline iterations

for M in range(1,NumberOfModes):
    print("M: ",M)
    Qmat=np.zeros((M,M)) #Triangular matrix
    for k1 in range(M):
        for k2 in range(M):
            Qmat[k1,k2]=qlist[k2][MagicPointIndiceslist[k1]]
    
    GGrid=np.zeros((basis.doflocs.shape[1],NumberOfSnapshots*NumberOfSnapshots)) #function g evaluated for all degree of freedom (doflocs) 
    GinftyGrid=np.zeros(NumberOfSnapshots*NumberOfSnapshots)
    for i,elem1 in enumerate((list(mu1))):
        for j,elem2 in enumerate((list(mu2))):
            mu=[elem1,elem2]
            Gvec=np.zeros(M)
            u=basis.project(g)
            for k1 in range(M):
                Gvec[k1]=u[MagicPointIndiceslist[k1]]
            alpha=np.linalg.solve(Qmat,Gvec)
            
            for k1 in range(M):
                u-=alpha[k1]*qlist[k1] #g-I[G] (to find the new basis function we retrieve the previous ones)
            
            GGrid[:,i*NumberOfSnapshots+j]=u 
            GinftyGrid[i*NumberOfSnapshots+j]=np.linalg.norm(u,np.inf)

    for elem in muIndexlist: # if already in the list (we could have used a dictionnary instead)
        GinftyGrid[elem]=-1e30
        
    muIndex=np.argmax(GinftyGrid) #find the mu* s.t argmax_mu ||g(mu)-I[g(mu)]||_inf
    muIndexlist.append(muIndex)
    
    mu1Index=int(np.floor(muIndex/NumberOfSnapshots))
    mu2Index=int(muIndex-mu1Index*NumberOfSnapshots)

    mu=[mu1[mu1Index],mu2[mu2Index]]
    g1=GGrid[:,muIndex].copy()
   
    for elem in MagicPointIndiceslist:
        g1[elem]=-1e30 #magic point not selected if already in the list
    
    XIndex=np.argmax(g1) #Find associated magic point: x* s.t argmax_x ||g(mu*)-I[g(mu*)]||_inf
   
    MagicPoint=[dof_locs[0][XIndex],dof_locs[1][XIndex]]
    print(" New magic point ", MagicPoint)
    
    qM= GGrid[:,muIndex]/GGrid[XIndex,muIndex] # New basis 
    qlist.append(qM)
    
    MagicPointlist.append(MagicPoint)
    MagicPointIndiceslist.append(XIndex)
M:  1
 New magic point  [0.03125, 0.15625]
M:  2
 New magic point  [0.1875, 0.03125]
M:  3
 New magic point  [0.9375, 0.03125]
M:  4
 New magic point  [0.03125, 0.9375]
M:  5
 New magic point  [0.03125, 0.0625]
M:  6
 New magic point  [0.40625, 0.03125]
M:  7
 New magic point  [0.0625, 0.03125]
M:  8
 New magic point  [0.15625, 0.0625]
M:  9
 New magic point  [0.96875, 0.96875]
M:  10
 New magic point  [0.21875, 0.4375]
M:  11
 New magic point  [0.03125, 0.40625]
M:  12
 New magic point  [0.03125, 0.0625]
M:  13
 New magic point  [0.21875, 0.1875]
M:  14
 New magic point  [0.65625, 0.03125]
# ONLINE PART !!

mu=[-0.5,-0.5]; #New targeted parameter!!
u=basis.project(g)

sol=0
Qmat=np.zeros((NumberOfModes,NumberOfModes))
for k1 in range(NumberOfModes):
    for k2 in range(NumberOfModes):
        if k2<=k1:    #triangular matrix
            Qmat[k1,k2]=qlist[k2][MagicPointIndiceslist[k1]]

Gvec=np.zeros(NumberOfModes)
for k1 in range(NumberOfModes):
    Gvec[k1]=basis.project(g)[MagicPointIndiceslist[k1]]

alpha=np.linalg.solve(Qmat,Gvec) # Find new alpha(nu) for this parameter

### Test online errors g(xseek,yseek,nu) ###
xseek=0.5; yseek=0.5;
element_finder = mesh.element_finder() 
# Find elements which contain (xseek, yseek)
element_index = element_finder(np.array([xseek]), np.array([yseek]))

for i in range(NumberOfModes):
    sol+=alpha[i]*qlist[i][element_index[0]] 

test=np.abs(sol-u[element_index[0]]);
print("//***************************************************(x,y)=("+str(xseek)+","+str(yseek)+")*********//")
print("//**    Approximation: "+str(sol)+" true solution: "+str(g([xseek,yseek]))+" error " +str(test)+"        **//")
print("//**    Approximation: "+str(sol)+" true solution: "+str(u[element_index[0]])+" error " +str(test)+"        **//")
//***************************************************(x,y)=(0.5,0.5)*********//
//**    Approximation: 0.7184530924757091 true solution: 0.7071067811865475 error 0.0001082689218868671        **//
//**    Approximation: 0.7184530924757091 true solution: 0.7183448235538222 error 0.0001082689218868671        **//