Tutorial: Building a Reduced Basis

In this tutorial we will learn more about VectorArrays and how to construct a reduced basis using pyMOR.

A reduced basis spans a low dimensional subspace of a Model’s solution_space, in which the solutions of the Model can be well approximated for all parameter values. In this context, time is treated as an additional parameter. So for time-dependent problems, the reduced space (the span of the reduced basis) should approximate the solution for all parameter values and time instances.

An upper bound for the possible quality of a reduced space is given by the so-called Kolmogorov \(N\)-width \(d_N\) given as

\[\begin{split}d_N := \inf_{\substack{V_N \subseteq V\\ \operatorname{dim}(V_N) \leq N}}\, \sup_{\mu \in \mathcal{P}}\, \inf_{v \in V_N}\, \|u(\mu) - v\|.\end{split}\]

In this formula \(V\) denotes the solution_space, \(\mathcal{P} \subset \mathbb{R}^p\) denotes the set of all parameter values we are interested in, and \(u(\mu)\) denotes the solution of the Model for the given parameter values \(\mu\). In pyMOR the set \(\mathcal{P}\) is called the ParameterSpace.

How to read this formula? For each candidate reduced space \(V_N\) we look at all possible parameter values \(\mu\) and compute the best-approximation error in \(V_N\) (the second infimum). The supremum over the infimum is thus the worst-case best-approximation error over all parameter values of interest. Now we take the infimum of the worst-case best-approximation errors over all possible reduced spaces of dimension at most \(N\), and this is \(d_N\).

So whatever reduced space of dimension \(N\) we pick, we will always find a \(\mu\) for which the best-approximation error in our space is at least \(d_N\). Reduced basis methods aim at constructing spaces \(V_N\) for which the worst-case best-approximation error is as close to \(d_N\) as possible.

However, we will only find a good \(V_N\) of small dimension \(N\) if the values \(d_N\) decrease quickly for growing \(N\). It can be shown that this is the case as soon as \(u(\mu)\) analytically depends on \(\mu\), which is true for many problems of interest. More precisely, it can be shown [BCDDPW11], [DPW13] that there are constants \(C, c > 0\) such that

\[d_N \leq C \cdot e^{-N^c}.\]

In this tutorial we will construct reduced spaces \(V_N\) for a concrete problem with pyMOR and study their error decay.

Model setup

First we need to define a Model and a ParameterSpace for which we want to build a reduced basis. We choose here the standard thermal block benchmark problem shipped with pyMOR (see Getting started). However, any pyMOR Model can be used, except for Section ‘Weak greedy algorithm’ where some more assumptions have to be made on the Model.

First we import everything we need:

import numpy as np
from pymor.basic import *

Then we build a 3-by-3 thermalblock problem that we discretize using pyMOR’s builtin discretizers (see Tutorial: Using pyMOR’s discretization toolkit for an introduction to pyMOR’s discretization toolkit).

problem = thermal_block_problem((3,3))
fom, _ = discretize_stationary_cg(problem, diameter=1/100)

Next, we need to define a ParameterSpace of parameter values for which the solutions of the full-order model fom should be approximated by the reduced basis. We do this by calling the space method of the Parameters of fom:

parameter_space = fom.parameters.space(0.0001, 1.)

Here, 0.0001 and 1. are the common lower and upper bounds for the individual components of all Parameters of fom. In our case fom expects a single parameter 'diffusion' of 9 values:

fom.parameters
Parameters({diffusion: 9})

If fom were to depend on multiple Parameters, we could also call the space method with a dictionary of lower and upper bounds per parameter.

The main use of ParameterSpaces in pyMOR is that they allow to easily sample parameter values from their domain using the methods sample_uniformly and sample_randomly.

Computing the snapshot data

Reduced basis methods are snapshot-based, which means that they build the reduced space as a linear subspace of the linear span of solutions of the fom for certain parameter values. The easiest approach is to just pick these values randomly, what we will do in the following. First we define a training set of 25 parameters:

training_set = parameter_space.sample_randomly(25)
print(training_set)
[Mu({diffusion: [0.37460266483547777, 0.9507192349792751, 0.7320207424172239, 0.5986986183486169, 0.15610303857839228, 0.15607892088416903, 0.05817780380698265, 0.8661895281603577, 0.6011549002420344]}), Mu({diffusion: [0.7081017705382658, 0.020682435846372867, 0.9699128611767781, 0.8324593965363417, 0.21241787676720833, 0.1819067847103799, 0.18348616940244847, 0.30431181873524177, 0.5248039559890746]}), Mu({diffusion: [0.43200182414025157, 0.2913000172840221, 0.6118917094329073, 0.13957991126597663, 0.2922154340703646, 0.3664252071093623, 0.4561243772186142, 0.7851974437968743, 0.1997538147801439]}), Mu({diffusion: [0.5142830149697702, 0.5924553274051563, 0.04654576767872573, 0.6075840974162482, 0.17060707127492278, 0.065145087825981, 0.9488906486996079, 0.9656354698712519, 0.8084165083816495]}), Mu({diffusion: [0.30468330779645336, 0.09776234679498323, 0.6842646032095057, 0.4402084784902273, 0.12212603102129435, 0.49522739242025904, 0.03448508226310688, 0.9093294700385742, 0.2588541036018569]}), Mu({diffusion: [0.6625560321255466, 0.311779904981802, 0.520116014375693, 0.5467556083153453, 0.18493597007997448, 0.9695876693017821, 0.7751553100787785, 0.9395049916700327, 0.8948378676926061]}), Mu({diffusion: [0.597940188813204, 0.9218820475996146, 0.0885836528017143, 0.1960632641329033, 0.04532276618164702, 0.325397797730188, 0.38873842196051306, 0.2714218968707185, 0.8287546354010141]}), Mu({diffusion: [0.3568176513609199, 0.28100641623641204, 0.5427418135499327, 0.14101013255226516, 0.8022167610559643, 0.07464318861540285, 0.9868882479068573, 0.7722675448197277, 0.19879580996601898]}), Mu({diffusion: [0.005621564911890039, 0.8154798823119886, 0.7068866581132324, 0.7290342673241832, 0.7712932196512772, 0.07413724726891696, 0.35852988197141816, 0.1159574726191772, 0.863117115533006]}), Mu({diffusion: [0.6233357970148752, 0.3309649350501639, 0.06365199445099504, 0.3110512234834906, 0.32525080369454434, 0.7296332177202303, 0.6375937156080776, 0.8872240213020689, 0.4722677036694331]}), Mu({diffusion: [0.11968228651370788, 0.7132734627442727, 0.7608089701120357, 0.5613210698497393, 0.7709900832365655, 0.4938462168047543, 0.5227805560990558, 0.42759826425671377, 0.025516584831420778]}), Mu({diffusion: [0.10798063785060512, 0.031526042768165584, 0.6364467702226541, 0.314424545478219, 0.5086198340955863, 0.9075757172787005, 0.24936729992596005, 0.41044188474332616, 0.7555755834291944]}), Mu({diffusion: [0.2288752856750733, 0.07707221183781011, 0.28982247776847664, 0.161305165125279, 0.9297046825773388, 0.8081395675264605, 0.6334404161347724, 0.871473444128699, 0.8036917096914246]}), Mu({diffusion: [0.18665140188014723, 0.8925697425901288, 0.5393883076914592, 0.8074594111485461, 0.8961016907935009, 0.3180716746243667, 0.11014091933522399, 0.22801236902568747, 0.42716507784739366]}), Mu({diffusion: [0.8180329644459009, 0.8607445101980178, 0.007051435318137585, 0.5107962278473079, 0.41746926204846413, 0.22218559968968316, 0.11995338079694944, 0.3376814098864876, 0.9429154129421279]}), Mu({diffusion: [0.32327061172755317, 0.5188387426811918, 0.7030486569992883, 0.36369323941905607, 0.9717849045126886, 0.962451050212617, 0.2518571175957816, 0.4972987810417962, 0.30094822198578797]}), Mu({diffusion: [0.2849120103280299, 0.03698325865979735, 0.6096033775464988, 0.5027287553265386, 0.05157360337486436, 0.27871859959018774, 0.9082750593780571, 0.2396379344779057, 0.14498038260401397]}), Mu({diffusion: [0.4895038150015352, 0.9856518890651896, 0.24213106598434925, 0.672168333851138, 0.7616434533671848, 0.2377137802380004, 0.7282435269769983, 0.3678463544059813, 0.6323426000105201]}), Mu({diffusion: [0.6335663577898186, 0.535821106606351, 0.09038074107740286, 0.8353189653396791, 0.32084798696523864, 0.18659985854881422, 0.040871064040608446, 0.590933853893923, 0.6775966054060982]}), Mu({diffusion: [0.016686170144963364, 0.512141848993451, 0.22657312562041815, 0.645208273130409, 0.17444899236209094, 0.6909686443286557, 0.38679667276590735, 0.9367363157378609, 0.13760719205157865]}), Mu({diffusion: [0.3411322444151535, 0.11356217388846501, 0.9247011489167349, 0.8773516194456429, 0.2580158335523841, 0.6600180476295756, 0.8172404779811957, 0.5552452915183024, 0.5296976132981709]}), Mu({diffusion: [0.24192810567136164, 0.09319345752911863, 0.8972260363775314, 0.9004280153576141, 0.6331381471275406, 0.3390958880695958, 0.3492746536551996, 0.7259830833023524, 0.8971205489265819]}), Mu({diffusion: [0.8870977156226908, 0.7798975583030381, 0.6420674429896723, 0.08423155099854933, 0.1617125512232043, 0.8985643331082266, 0.606468416753624, 0.009296131911467984, 0.10156139571174551]}), Mu({diffusion: [0.663535418931145, 0.005161077687834065, 0.16089197061235688, 0.5487789159876495, 0.6919260081729239, 0.6519960633766503, 0.2243468825296137, 0.7122080034254011, 0.23732536258805037]}), Mu({diffusion: [0.32546715818945177, 0.7465167559775123, 0.64966793575731, 0.8492384881531285, 0.6576471310111133, 0.568351772475138, 0.09376540035130967, 0.36777903147912755, 0.26527584744495725]})]

Then we solve the full-order model for all parameter values in the training set and accumulate all solution vectors in a single VectorArray using its append method. But first we need to create an empty VectorArray to which the solutions can be appended. New VectorArrays in pyMOR are always created by a corresponding VectorSpace. Empty arrays are created using the empty method. But what is the right VectorSpace? All solutions of a Model belong to its solution_space, so we write:

U = fom.solution_space.empty()
for mu in training_set:
    U.append(fom.solve(mu))

Note that fom.solve returns a VectorArray containing a single vector. This entire array (of one vector) is then appended to the U array. pyMOR has no notion of single vectors, we only speak of VectorArrays.

What exactly is a VectorSpace? A VectorSpace in pyMOR holds all information necessary to build VectorArrays containing vector objects of a certain type. In our case we have

fom.solution_space
NumpyVectorSpace(20201, id='STATE')

which means that the created VectorArrays will internally hold NumPy arrays for data storage. The number is the dimension of the vector. We have here a NumpyVectorSpace because pyMOR’s builtin discretizations are built around the NumPy/SciPy stack. If fom would represent a Model living in an external PDE solver, we would have a different type of VectorSpace which, for instance, might hold a reference to a discrete functions space object inside the PDE solver instead of the dimension.

After appending all solutions vectors to U, we can verify that U now really contains 25 vectors:

len(U)
25

Note that appending one VectorArray V to another array U will append copies of the vectors in V to U. So modifying U will not affect V.

Let’s look at the solution snapshots we have just computed using the visualize method of fom. A VectorArray containing multiple vectors is visualized as a time series:

fom.visualize(U)