Online Phase: PBDW

This notebook implements the online phase of the following algorithm:

  • Parameterised-Background Data-Weak (PBDW) formulation

[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
from dolfinx.fem import FunctionSpace

from pyforce.tools.write_read import ImportH5
from pyforce.tools.functions_list import FunctionsList
from pyforce.online.pbdw_synthetic import PBDW

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

# 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]:
bf = dict()

for field_i in range(len(var_names)):

    bf[var_names[field_i]] = ImportH5(V, path_off+'/BasisFunctions/basisPOD_'+var_names[field_i], 'POD_'+var_names[field_i])[0]

s = [0.1, 1., 2.5]

is_H1 = [False, True]
fun_space_label = ['L2', 'H1']

bs = dict()

for field in var_names:
    bs[field] = dict()
    for s_jj in s:
        bs[field]['s = {:.2f}'.format(s_jj)] = dict()
        for space in fun_space_label:
             bs[field]['s = {:.2f}'.format(s_jj)][space] = ImportH5(V,
                                                                    path_off+'/BasisSensors/sensorsSGREEDYPOD_' + field+'_s_{:.2e}_'.format(s_jj)+space,
                                                                    'SGREEDYPOD_' +field+'_s_{:.2e}'.format(s_jj))[0]

Online Reconstruction

In this section, the potentialities of PBDW are analysed: focusing on the effect of the Riesz representation.

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.

[5]:
pbdw_online = dict()

for field in var_names:
    pbdw_online[field] = dict()
    for s_jj in s:
        pbdw_online[field]['s = {:.2f}'.format(s_jj)] = dict()
        for kk, space in enumerate(fun_space_label):
            pbdw_online[field]['s = {:.2f}'.format(s_jj)][space] = PBDW(bf[field],
                                                                        bs[field]['s = {:.2f}'.format(s_jj)][space],
                                                                        field, is_H1=is_H1[kk])

Let us compute the test errors for a single value of random noise \(\sigma\), if the noise value increases too much the hyperparameter \(\xi\) must be tuned properly as in Maday and Taddei (2019).

[6]:
noise_value = 1e-6

Nmax = 10
Mmax = 30

test_errors = dict()
comput_time = dict()

print('----------------------------')
for field_i, field in enumerate(var_names):
    print('Reconstructing '+field)
    test_errors[field] = dict()
    comput_time[field] = dict()
    for jj, s_jj in enumerate(s):
        print('     s = {:.2f}'.format(s[jj]))

        test_errors[field]['s = {:.2f}'.format(s_jj)] = dict()
        comput_time[field]['s = {:.2f}'.format(s_jj)] = dict()

        for kk, space in enumerate(fun_space_label):
            print('          - '+space)

            out = pbdw_online[field]['s = {:.2f}'.format(s_jj)][space].synt_test_error(test_snaps[field_i],
                                                                                       N=Nmax, M = Mmax,
                                                                                       noise_value = noise_value)

            test_errors[field]['s = {:.2f}'.format(s_jj)][space] = [out[0], out[1]]
            comput_time[field]['s = {:.2f}'.format(s_jj)][space] = out[2]
            del out

    print('----------------------------')
----------------------------
Reconstructing phi_1
     s = 0.10
          - L2
          - H1
     s = 1.00
          - L2
          - H1
     s = 2.50
          - L2
          - H1
----------------------------
Reconstructing phi_2
     s = 0.10
          - L2
          - H1
     s = 1.00
          - L2
          - H1
     s = 2.50
          - L2
          - H1
----------------------------

Let us plot the test error.

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

tex_fun_space_label = [r'$L^2$', r'$H^1$']

for field_i, field in enumerate(var_names):
    fig, axs = plt.subplots(nrows = len(is_H1), ncols = len(s), sharey = True, sharex = True, figsize = (6 * len(s), 5 * len(is_H1)))

    for jj, s_jj in enumerate(s):

        linestyles = ['-o', '-^']
        colors = cm.jet(np.linspace(0.1, 0.9, len(fun_space_label)))
        for kk, space in enumerate(fun_space_label):

            for err_i in range(2):
                axs[err_i, jj].semilogy(Mplot, test_errors[field]['s = {:.2f}'.format(s_jj)][space][err_i], linestyles[kk],
                                    c=colors[kk], label=tex_fun_space_label[kk])

                axs[err_i, jj].grid(which = 'major', linestyle = '-')
                axs[err_i, jj].grid(which = 'minor', linestyle = '--')
                axs[err_i, jj].set_xlim(Nmax, Mmax)
                axs[err_i, jj].legend(framealpha=1, fontsize=20)
                axs[err_i, jj].tick_params(axis='both', labelsize=20)

                axs[0, jj].set_title(r'Point Spread $s={:.2f}$'.format(s[jj]), fontsize=20)
                axs[-1, jj].set_xlabel(r'Number of Sensors $M$', fontsize=20)

    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)

    fig.subplots_adjust(hspace = 0.05, wspace = 0.1)
../../_images/Tutorials_02_MGDiffusion_03b_online_PBDW_13_0.png
../../_images/Tutorials_02_MGDiffusion_03b_online_PBDW_13_1.png

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

[40]:
jj = 1


# Initialize subplots
fig, axs = plt.subplots(nrows = 1, ncols = len(fun_space_label), sharey = True, figsize=(12, 5))


for idx_ax, algo in enumerate(fun_space_label):

    # 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[field]['s = {:.2f}'.format(s_jj)][algo].keys()):
            mean = np.mean(np.mean(comput_time[field]['s = {:.2f}'.format(s_jj)][algo][key], axis=0))
            std = np.std(np.mean(comput_time[field]['s = {:.2f}'.format(s_jj)][algo][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[field]['s = {:.2f}'.format(s_jj)][algo].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'Riesz in '+tex_fun_space_label[idx_ax]+ ' - $s={:.2f}$'.format(s[jj]), fontsize=20)
    else:
        axs[idx_ax].set_title(r'Riesz in '+tex_fun_space_label[idx_ax]+ ' - $s={:.2f}$'.format(s[jj]), fontsize=20)

    axs[idx_ax].set_xticks(ind)
    axs[idx_ax].set_xticklabels(list(comput_time[field]['s = {:.2f}'.format(s_jj)][algo].keys()))
    axs[idx_ax].legend(framealpha=1, fontsize=20)
    axs[idx_ax].tick_params(axis='both', labelsize=18)
    axs[idx_ax].grid()

fig.subplots_adjust(wspace=0.35)
../../_images/Tutorials_02_MGDiffusion_03b_online_PBDW_15_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.

[41]:
import pandas as pd

import vtk
import pyvista as pv
import dolfinx

pv.start_xvfb()
[42]:
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.

[59]:
Mmax = 20
noise_value = 1e-6

jj = 1

rom_recs = list()

for field_i, field in enumerate(var_names):
    rom_recs.append({'L2': FunctionsList(V),
                     'H1': FunctionsList(V)})

    for mu in range(len(test_snaps[field_i])):
        for kk, space in enumerate(fun_space_label):

            out = pbdw_online[field]['s = {:.2f}'.format(s_jj)][space].reconstruct(test_snaps[field_i](mu),
                                                                                   N = Nmax, M = Mmax, noise_value = noise_value,
                                                                                   )
            rom_recs[field_i][space].append(out[0])
            del out

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

[60]:
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_data = [pd.read_excel('../BenchmarkData/MGDiffusion_ANL11A2/anl11a2_data.xlsx', sheet_name='x-axis').to_numpy()/1000,
              pd.read_excel('../BenchmarkData/MGDiffusion_ANL11A2/anl11a2_data.xlsx', 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_03b_online_PBDW_22_0.png

Let us make some contour plots

[61]:
for field_i in range(len(var_names)):
    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_03b_online_PBDW_24_0.png
../../_images/Tutorials_02_MGDiffusion_03b_online_PBDW_24_1.png