Online Phase: GEIM algorithm - effect of random noise

This notebook implements the online phase of the following algorithms: - Generalised Empirical Interpolation Method (GEIM) - Tikhonov Regularisation Generalised Empirical Interpolation Method (TR-GEIM)

In particular, both clean and noisy data will be considered. The GEIM algorithm is unstable with respect to random noise, thus a stabilisation method is applied based on Tikhonov Regularisation.

[1]:
import numpy as np
import os
from IPython.display import clear_output
import pickle

import gmsh
from mpi4py import MPI
from dolfinx.io.gmshio import model_to_mesh,read_from_msh
from dolfinx.fem import FunctionSpace

from pyforce.tools.write_read import ImportH5, StoreFunctionsList as store
from pyforce.tools.functions_list import FunctionsList
from pyforce.online.geim_synthetic import GEIM
from pyforce.online.tr_geim_synthetic import TRGEIM

import matplotlib.pyplot as plt
from matplotlib import rcParams
from matplotlib import cm

path_off ='./Offline_results/'

The geometry is imported from “ANL11A2_octave.geo”, generated with GMSH. Then, the mesh is created with the gmsh module.

[2]:
gdim = 2

model_rank = 0
mesh_comm = MPI.COMM_WORLD

use_msh = False

# If the snapshots are generated from a different computer than the one used for the offline/online simulation, there may be issues in the number of dofs
if (use_msh):
    domain, ct, ft = read_from_msh("ANL11A2_octave.msh", comm = mesh_comm, rank = model_rank, gdim = gdim)
else:
    # Initialize the gmsh module
    gmsh.initialize()

    # Load the .geo file
    gmsh.merge('ANL11A2_octave.geo')
    gmsh.model.geo.synchronize()

    # Set algorithm (adaptive = 1, Frontal-Delaunay = 6)
    gmsh.option.setNumber("Mesh.Algorithm", 6)

    gmsh.model.mesh.generate(gdim)
    gmsh.model.mesh.optimize("Netgen")

    # Domain
    domain, ct, ft = model_to_mesh(gmsh.model, comm = mesh_comm, rank = model_rank, gdim = gdim )
    gmsh.finalize()

fuel1_marker    = 1
fuel2_marker    = 2
fuel_rod_marker = 3
refl_marker     = 4

void_marker     = 10
sym_marker      = 20

tdim = domain.topology.dim
fdim = tdim - 1
domain.topology.create_connectivity(fdim, tdim)

clear_output()

Importing Snapshots

The snapshots are loaded and stored into suitable data structures.

[3]:
# Defining the functional space
V = FunctionSpace(domain, ("Lagrange", 1))

# Define the variables to load
var_names = [
             'phi_1',
             'phi_2'
             ]

tex_var_names = [
                 r'\phi_1',
                 r'\phi_2'
                 ]

# Snapshot path
path_FOM = './Snapshots/'

################ Importing Snapshots ########################
test_snaps = list()

for field_i in range(len(var_names)):
    test_snaps.append(FunctionsList(V))

    tmp_FOM_list, _ = ImportH5(V, path_FOM+'test_snap_'+var_names[field_i], var_names[field_i])

    for mu in range(len(tmp_FOM_list)):
        test_snaps[field_i].append(tmp_FOM_list(mu))

    del tmp_FOM_list

test_params = list()
for field_i in range(len(var_names)):
    with open(path_FOM+'./test.param', 'rb') as f:
        test_params.append(pickle.load(f))

Let us import the magic functions and sensors

[4]:
mf = dict()
ms = dict()
coeffs_geim = dict()

s = [0.1, 1., 2.5]

for field_i in range(len(var_names)):

    mf[var_names[field_i]] = [ImportH5(V, path_off+'BasisFunctions/basisGEIM_' + var_names[field_i]+'_s_{:.2e}'.format(s[jj]), 'GEIM_' +var_names[field_i]+'_s_{:.2e}'.format(s[jj]))[0] for jj in range(len(s))]
    ms[var_names[field_i]] = [ImportH5(V, path_off+'BasisSensors/sensorsGEIM_' + var_names[field_i]+'_s_{:.2e}'.format(s[jj]), 'GEIM_' +var_names[field_i]+'_s_{:.2e}'.format(s[jj]))[0] for jj in range(len(s))]
    coeffs_geim[var_names[field_i]] = [np.loadtxt(path_off+'coeffs/GEIM_'+var_names[field_i]+'_s_{:.2e}.txt'.format(s[jj]), delimiter=',') for jj in range(len(s))]

The Tikhonov Regularisation for GEIM weakens the interpolation condition \begin{equation*} \mathbb{B}\boldsymbol{\beta} = \mathbf{y} \quad \Longrightarrow \quad \left(\mathbb{B}^T\mathbb{B}+\lambda \mathbb{T}^T\mathbb{T}\right)\boldsymbol{\beta} = \mathbb{B}^T\mathbf{y}+\lambda \mathbb{T}^T\mathbb{T} \overline{\boldsymbol{\beta}} \end{equation*} where \(\lambda\in\mathbb{R}^+\) is a regularisation parameter to be suitably calibrated, \(\overline{\boldsymbol{\beta}}\) is the sample mean of the coefficients of the train set (\(\Xi_{\text{train}}\)), \(\mathbb{T}\in\mathbb{R}^{M\times M}\) is the regularisation matrix: \begin{equation} \begin{split} \mathbb{T}_{ij} =\left\{ \begin{array}{cc} |\sigma_{\beta_i}|^{-1} & \text{ if } i=j\\ 0 & \text{ if } i\neq j\\ \end{array} \right.\qquad i,j = 1, \dots, M. \end{split} \end{equation} with \(\sigma_{\beta_i}\) as the sample standard-deviation of the training coefficients.

[5]:
trgeim_coeffs = dict()

for field in var_names:
    trgeim_coeffs[field] = list()
    for jj in range(len(s)):
        trgeim_coeffs[field].append(dict())

        trgeim_coeffs[field][jj]['mean'] = np.mean(coeffs_geim[field][jj], axis=0)
        trgeim_coeffs[field][jj]['std']  = np.std(coeffs_geim[field][jj],  axis=0)

Online Reconstruction

In this section, the potentialities of GEIM and its extension to deal with random noise will be analysed: in particular, the effect of the random noise on the average test error and the interpolant.

Effect of random noise

The measures are polluted by synthetic random noise (modelled as uncorrelated Gaussian noise) \begin{equation*} \{y_m = v_m(u)+\epsilon_m\}_{m=1}^M \qquad \text{ with }\epsilon_m \sim \mathcal{N}(0,\sigma^2) \end{equation*} In this section, the test errors will be computed as \begin{equation*} \begin{split} E_M[u] &= \left\langle \left\| u(\cdot;\,\boldsymbol{\mu}) - \mathcal{P}_M[u](\cdot;\,\boldsymbol{\mu})\right\|_{L^2(\Omega)}\right\rangle_{\boldsymbol{\mu}\in\Xi^{test}}\\ \varepsilon_M[u] &= \left\langle \frac{\left\| u(\cdot;\,\boldsymbol{\mu}) - \mathcal{P}_M[u](\cdot;\,\boldsymbol{\mu})\right\|_{L^2(\Omega)}}{\left\| u(\cdot;\,\boldsymbol{\mu}) \right\|_{L^2(\Omega)}}\right\rangle_{\boldsymbol{\mu}\in\Xi^{test}} \end{split} \end{equation*} given \(\mathcal{P}_M[u]\) the reconstruction operator with \(M\) magic functions.

[6]:
geim_online    = dict()
tr_geim_online = dict()

for field in var_names:
    geim_online[field]    = [GEIM(mf[field][jj], ms[field][jj], field) for jj in range(len(s))]
    tr_geim_online[field] = [TRGEIM(mf[field][jj], ms[field][jj], trgeim_coeffs[field][jj]['mean'], trgeim_coeffs[field][jj]['std'], field) for jj in range(len(s))]

Let us compute the test errors for different values of random noise \(\sigma\).

[7]:
sigma = [None, 1e-6, 1e-5, 1e-4]
Mmax  = 30

test_errors = {'geim': dict(), 'trgeim': dict()}
comput_time = {'geim': dict(), 'trgeim': dict()}

print('----------------------------')
for field_i, field in enumerate(var_names):

    print('Reconstructing '+field)

    test_errors['geim'][field]   = list()
    test_errors['trgeim'][field] = list()
    comput_time['geim'][field]   = list()
    comput_time['trgeim'][field] = list()

    for jj in range(len(s)):

        test_errors['geim'][field].append(list())
        test_errors['trgeim'][field].append(list())
        comput_time['geim'][field].append(list())
        comput_time['trgeim'][field].append(list())
        for idx_noise in range(len(sigma)):

            print('     s = {:.2f}'.format(s[jj])+' - sigma = '+str(sigma[idx_noise]))

            # GEIM
            out = geim_online[field][jj].synt_test_error(test_snaps[field_i], M = Mmax, noise_value = sigma[idx_noise], verbose = False)

            test_errors['geim'][field][jj].append(np.zeros((2,Mmax)))
            test_errors['geim'][field][jj][idx_noise][0] = out[0][:]
            test_errors['geim'][field][jj][idx_noise][1] = out[1][:]

            comput_time['geim'][field][jj].append(out[2])

            del out

            # TR-GEIM
            if sigma[idx_noise] is not None:
                out = tr_geim_online[field][jj].synt_test_error(test_snaps[field_i], M = Mmax,
                                                                noise_value = sigma[idx_noise], reg_param=sigma[idx_noise]**2,
                                                                verbose = False)

                test_errors['trgeim'][field][jj].append(np.zeros((2,Mmax)))
                test_errors['trgeim'][field][jj][idx_noise-1][0] = out[0][:]
                test_errors['trgeim'][field][jj][idx_noise-1][1] = out[1][:]

                comput_time['trgeim'][field][jj].append(out[2])

                del out

    print('----------------------------')
----------------------------
Reconstructing phi_1
     s = 0.10 - sigma = None
     s = 0.10 - sigma = 1e-06
     s = 0.10 - sigma = 1e-05
     s = 0.10 - sigma = 0.0001
     s = 1.00 - sigma = None
     s = 1.00 - sigma = 1e-06
     s = 1.00 - sigma = 1e-05
     s = 1.00 - sigma = 0.0001
     s = 2.50 - sigma = None
     s = 2.50 - sigma = 1e-06
     s = 2.50 - sigma = 1e-05
     s = 2.50 - sigma = 0.0001
----------------------------
Reconstructing phi_2
     s = 0.10 - sigma = None
     s = 0.10 - sigma = 1e-06
     s = 0.10 - sigma = 1e-05
     s = 0.10 - sigma = 0.0001
     s = 1.00 - sigma = None
     s = 1.00 - sigma = 1e-06
     s = 1.00 - sigma = 1e-05
     s = 1.00 - sigma = 0.0001
     s = 2.50 - sigma = None
     s = 2.50 - sigma = 1e-06
     s = 2.50 - sigma = 1e-05
     s = 2.50 - sigma = 0.0001
----------------------------

Let us plot the test error, to observe the instability of the GEIM algorithm with respect to random noise (i.e., the error increases as the noise level increases).

[20]:
ls = 2
Mplot = np.arange(1, Mmax+1,1)

for field_i, field in enumerate(var_names):

    fig, axs = plt.subplots(nrows = 2, ncols = len(s), sharex = True, figsize = (6 * len(s), 5 * 2))

    for jj in range(len(s)):

        colors = cm.viridis(np.linspace(0.,.95,len(sigma)))

        for err in range(2):
            for idx_noise in range(len(sigma)):
                if sigma[idx_noise] is None:
                    label = r'No Noise'
                else:
                    label = r'$\sigma = 10^{'+str(int(np.log10(sigma[idx_noise])))+'}$'
                axs[err, jj].semilogy(Mplot, test_errors['geim'][field][jj][idx_noise][err],         linewidth=ls, c = colors[idx_noise], label = 'GEIM - '+label)

    for jj in range(len(s)):
        colors = cm.viridis(np.linspace(0.,.95,len(sigma)))
        for err in range(2):
            for idx_noise in range(len(sigma)):
                if sigma[idx_noise] is not None:
                    label = r'$\sigma = 10^{'+str(int(np.log10(sigma[idx_noise])))+'}$'
                    axs[err, jj].semilogy(Mplot, test_errors['trgeim'][field][jj][idx_noise - 1][err], '--', linewidth=ls, c = colors[idx_noise], label = 'TR-GEIM - '+label)
            axs[err, jj].grid(which = 'major', linestyle = '-')
            axs[err, jj].grid(which = 'minor', linestyle = '--')
            axs[err, jj].set_xlim(1, Mmax)
        axs[0, jj].set_title(r'Point Spread $s={:.2f}$'.format(s[jj]), fontsize=20)

        axs[1, jj].set_xlabel(r'Number of Measures $M$', fontsize=25)

    axs[0,0].set_ylabel(r'Absolute Error $E_M['+tex_var_names[field_i]+']$', fontsize=25)
    axs[1,0].set_ylabel(r'Relative Error $\epsilon_M['+tex_var_names[field_i]+']$', fontsize=25)

    Lines, Labels = axs[0,0].get_legend_handles_labels()

    fig.legend(Lines, Labels, framealpha=1, loc=(0.05, 0.9), ncols = len(sigma), fontsize=20)
    fig.subplots_adjust(hspace = 0.05, wspace = 0.15, top = 0.875)
../../_images/Tutorials_02_MGDiffusion_03a_online_GEIM_16_0.png
../../_images/Tutorials_02_MGDiffusion_03a_online_GEIM_16_1.png

In the end, let us plot the computational time required by the Online Phase.

[32]:
jj = 1
idx_noise = 2

# Initialize subplots
fig, axs = plt.subplots(nrows = 1, ncols = 2, figsize=(12, 5))


for idx_ax in range(len(axs)):
    algo = list(comput_time.keys())[idx_ax]

    # Iterate over field_i values
    colors = plt.cm.viridis(np.linspace(0.4, 0.8, len(var_names)))  # Choose a colormap
    for field_i, color in zip(range(len(var_names)), colors):
        means = []
        stds = []
        field = var_names[field_i]

        # Calculate mean and std for each key
        for key in list(comput_time[algo][field][jj][idx_noise].keys()):
            mean = np.mean(np.mean(comput_time[algo][field][jj][idx_noise][key], axis=0))
            std = np.std(np.mean(comput_time[algo][field][jj][idx_noise][key], axis=0))
            means.append(mean)
            stds.append(std)

        # Plot the bar chart with error bars for standard deviation
        bar_width = 0.2  # Adjust as needed
        ind = np.arange(len(list(comput_time[algo][field][jj][idx_noise].keys())))
        bars = axs[idx_ax].bar(ind + (field_i - len(var_names) / 4) * bar_width, means, bar_width, label=r'$'+tex_var_names[field_i]+'$', color=color, yerr=stds, capsize=5)

    if idx_ax == 0:
        axs[idx_ax].set_ylabel(r'CPU Time (s)', fontsize=20)
        axs[idx_ax].set_title(r'GEIM - $s={:.2f}$'.format(s[jj])+' and $\sigma= 10^{'+str(int(np.log10(sigma[idx_noise])))+'}$', fontsize=20)
    else:
        axs[idx_ax].set_title(r'TR-GEIM - $s={:.2f}$'.format(s[jj])+' and $\sigma= 10^{'+str(int(np.log10(sigma[idx_noise])))+'}$', fontsize=20)

    axs[idx_ax].set_xticks(ind)
    axs[idx_ax].set_xticklabels(list(comput_time[algo][field][jj][idx_noise].keys()))
    axs[idx_ax].legend(framealpha=1, fontsize=20)
    axs[idx_ax].tick_params(axis='both', labelsize=18)
    axs[idx_ax].grid()
../../_images/Tutorials_02_MGDiffusion_03a_online_GEIM_18_0.png

Plotting the interpolant and the residual fields

Using pyvista and some additional functions the interpolant and its residual field \(r[u] = | u - \mathcal{P}[u]|\) is plotted.

[ ]:
import pandas as pd

import vtk
import pyvista as pv
import dolfinx

pv.start_xvfb()
[70]:
def grids(fun: dolfinx.fem.Function):
    topology, cell_types, geometry = dolfinx.plot.create_vtk_mesh(fun.function_space)
    u_grid = pv.UnstructuredGrid(topology, cell_types, geometry)
    u_grid.point_data['fun'] = fun.x.array[:].real
    u_grid.set_active_scalars('fun')
    return u_grid

def PlotFOM_vs_ROM( fom: FunctionsList, rom: dict, mu: int, title: str, varname: str,
                    clim = None, colormap = cm.jet,
                    colormap_res = cm.plasma_r, clim_res = None):

    keys = list(rom.keys())

    resolution = [1200, 600 * ( len(keys) + 1)]
    plotter = pv.Plotter(shape=(len(keys)+1, 2), off_screen=False, border=False, window_size=resolution)

    lab_fontsize = 32
    title_fontsize = 40
    zoom = 1.05

    dict_cb = dict(title = varname, width = 0.75, height = 0.1,
                    title_font_size=title_fontsize,
                    label_font_size=lab_fontsize,
                    n_labels=3,
                    color = 'k',
                    position_x=0.125, position_y=0.8,
                    shadow=False)

    ############################ FOMs ###################################

    plotter.subplot(0,0)

    warped_fom = grids(fom.map(mu))
    if clim is None:
        clim = [min(fom(mu)), max(fom(mu))]

    dict_cb['title'] = 'FOM - $'+varname+'$'
    plotter.add_mesh(warped_fom, clim = clim, cmap = colormap, show_edges=False, scalar_bar_args=dict_cb)
    plotter.view_xy()
    plotter.camera.zoom(zoom)

    ############################ ROMs ###################################


    for key_i in range(len(keys)):
        plotter.subplot(1+key_i,0)

        warped_rom = grids(rom[keys[key_i]].map(mu))

        dict_cb['title'] = keys[key_i]+' - $'+varname+'$'
        plotter.add_mesh(warped_rom, clim = clim, cmap = colormap, show_edges=False, scalar_bar_args=dict_cb)
        plotter.view_xy()
        plotter.camera.zoom(zoom)

    ############################ Residuals ###################################

    max_res = 0.
    for key_i in range(len(keys)):
        plotter.subplot(1+key_i,1)

        residual = dolfinx.fem.Function(rom[keys[key_i]].fun_space)
        residual.x.array[:] = np.abs(rom[keys[key_i]](mu) - fom(mu))

        max_res = max([max_res, max(residual.x.array[:])])

        warped_rom = grids(residual)

        if clim_res is None:
            clim_res = [0, max_res]

        dict_cb['title'] = 'Residual '+keys[key_i]+' - $'+varname+'$'
        plotter.add_mesh(warped_rom, clim = clim_res, cmap = colormap_res, show_edges=False, scalar_bar_args=dict_cb)
        plotter.view_xy()
        plotter.camera.zoom(zoom)


    plotter.set_background('white', top='white')
    plotter.subplot(0,1)
    plotter.add_text(str(title), color= 'k', position=[50, 300], font_size=25)

    ## Save figure
    plotter.show()

Let us reconstruct one of the fields for the whole test set, using some functions of the GEIM and TR-GEIM classes.

[72]:
Mmax = 20

jj = 1
noise_value = 1e-4

rom_recs = list()

for field_i in range(len(var_names)):
    field = var_names[field_i]



    rom_recs.append({'GEIM': FunctionsList(V),
                    'TR-GEIM': FunctionsList(V)})

    for mu in range(len(test_snaps[field_i])):
        out_geim = geim_online[field][jj].reconstruct(test_snaps[field_i](mu), M = Mmax, noise_value = noise_value)
        rom_recs[field_i]['GEIM'].append(out_geim[0])

        out_trgeim = tr_geim_online[field][jj].reconstruct(test_snaps[field_i](mu), M = Mmax, noise_value = noise_value, reg_param = noise_value**2)
        rom_recs[field_i]['TR-GEIM'].append(out_trgeim[0])

        del out_geim, out_trgeim

Let us compare the fast and thermal flux with respect to some benchmark data.

[166]:
def extract_cells(domain, points):

    bb_tree = dolfinx.geometry.BoundingBoxTree(domain, domain.topology.dim)
    cells = []
    points_on_proc = []
    cell_candidates = dolfinx.geometry.compute_collisions(bb_tree, points.T)
    colliding_cells = dolfinx.geometry.compute_colliding_cells(domain, cell_candidates, points.T)
    for i, point in enumerate(points.T):
        if len(colliding_cells.links(i))>0:
            points_on_proc.append(point)
            cells.append(colliding_cells.links(i)[0])
    xPlot = np.array(points_on_proc, dtype=np.float64)

    return xPlot, cells

Nhplot = 1000
xMax = 170
x_line = np.linspace(0, xMax + 1e-20, Nhplot)
points = [np.zeros((3, Nhplot)),
          np.zeros((3, Nhplot))]
points[0][0] = x_line
points[1][0] = x_line
points[1][1] = x_line

extracted = [extract_cells(domain, point) for point in points]

## Uploading data from benchmark

bench_path = '..//BenchmarkData/MGDiffusion_ANL11A2/anl11a2_data.xlsx'

bench_data = [pd.read_excel(bench_path, sheet_name='x-axis').to_numpy()/1000,
              pd.read_excel(bench_path, sheet_name='Diagonal').to_numpy()/1000]

bench_labels = [r'$y=x$', r'$y=0$']

mark_size = 10
ls = 2
tickssize = 20

mu_bench = 220

fluxFigure, axs = plt.subplots(nrows = 2, ncols = len(var_names), figsize=(6 * len(var_names), 8))

for bench_i in range(2):
    xPlot = extracted[bench_i][0]
    cells = extracted[bench_i][1]
    for field_i in range(len(var_names)):
        colors = cm.jet(np.linspace(0.1,1, len(list(rom_recs[field_i].keys()))+2))
        axs[bench_i, field_i].plot(bench_data[bench_i][:, 0], bench_data[bench_i][:, field_i+bench_i+1] / max(bench_data[bench_i][:, field_i+bench_i+1]), '^',
                                   c=colors[0], label = r'Benchmark', markersize=mark_size)
        axs[bench_i, field_i].plot(xPlot[:,0], test_snaps[field_i].map(mu_bench).eval(xPlot, cells) / max(test_snaps[field_i].map(mu_bench).eval(xPlot, cells)),
                                   c=colors[1], label = r'FOM', linewidth=ls)

        for algo_i, algo in enumerate(list(rom_recs[field_i].keys())):
            axs[bench_i, field_i].plot(xPlot[:,0], rom_recs[field_i][algo].map(mu_bench).eval(xPlot, cells) / max(rom_recs[field_i][algo].map(mu_bench).eval(xPlot, cells)),
                                       '--', c=colors[2+algo_i], label = algo, linewidth=ls)


        axs[bench_i, field_i].grid(which='major',linestyle='-')
        axs[bench_i, field_i].grid(which='minor',linestyle='--')
        axs[bench_i, field_i].set_ylabel(r"$\tilde{"+tex_var_names[field_i]+"}$ at "+bench_labels[bench_i], fontsize=20)

        if bench_i + 1 == 2:
            axs[bench_i, field_i].set_xlabel(r"$y\,[cm]$",fontsize=20)

Lines, Labels = axs[0,0].get_legend_handles_labels()
fluxFigure.legend(Lines, Labels, framealpha=1, loc=(0.2, 0.935), ncols = 2+len(rom_recs[0]), fontsize=15)
fluxFigure.subplots_adjust(wspace=0.2, hspace=0.1, top = 0.91)
../../_images/Tutorials_02_MGDiffusion_03a_online_GEIM_25_0.png

Let us make some contour plots

[168]:
field_i = 1

PlotFOM_vs_ROM(test_snaps[field_i], rom_recs[field_i], mu = mu_bench, title='Test Set: $\mu = '+str(mu)+'$',
               varname=tex_var_names[field_i])
../../_images/Tutorials_02_MGDiffusion_03a_online_GEIM_27_0.png