How do I use Pressio to build and run a ROM?

This tutorial is designed to show the entire, end-to-end process of building and running a reduced-order model with Pressio.

We recommend reading this step-by-step explanation alongside the source code itself, which is fully commented to provide additional context. The source code for this tutorial can be found in $SRC/full-tutorial/burgers.cpp. It is also included at the bottom of this page for reference.

Each step found below (and in the Table of Contents to the right) corresponds to the same-named section of the source code. After Step 0, we recommend you jump to the main function and follow along with the steps as they are explained.

Note

You’ll notice that the Pressio examples, this one included, favor the auto keyword for type deduction. This is because the types in Pressio are often complex template types that would be cumbersome to write out in full. We strongly recommend using auto when working with Pressio to simplify your code and ensure you avoid mistakes with type declarations.

Step 0: Pressio Setup

Pressio is a header-only library that provides building blocks to create reduced-order models (ROMs) for large-scale dynamical systems.

Because it is header-only, we depend on user-set macros to enable or disable features at compile time. These macros may be set via CMake configuration or directly in the source code before including any Pressio headers.

A complete discussion of Pressio’s macros can be found in the pressio-rom documentation.

In this tutorial, we simply define the key macros in the source code, like so:

#define PRESSIO_ENABLE_LOGGING
#define PRESSIO_ENABLE_TPL_EIGEN

Once the macros have been defined, we can include the core Pressio header.

#include "pressio/rom.hpp"

Since rom is the top-level module of the Pressio ecosystem, including this file will pull in all of Pressio–from the built-in logger to the low-level ops to the ODE steppers. One #include to rule them all.

From here, we just include a couple other header files–one with basic helper functions for this tutorial, and another with utilities for performing hyper-reduction later on.

#include "helpers.h"
#include "hyperreduction.h"

Next, we’ll also include the standard C++ vector header to store our snapshots.

#include <vector>

And finally, we’ll define some convenient type aliases for vectors and matrices using the Eigen library, which Pressio supports out of the box.

using vector_t = Eigen::VectorXd;
using matrix_t = Eigen::MatrixXd;

Now, we can jump down to the main function to start the tutorial.

First, we initialize the Pressio logger so that we can see output from the library as we go:

PRESSIOLOG_INITIALIZE(pressiolog::LogLevel::info);

You can change the LogLevel argument to debug for more information, or sparse for less.

Then we’ll just define some types for convenience:

using FomSystem = Burgers1dFom;
using time_t = typename FomSystem::time_type;

This FomSystem type is our implementation of the 1-D Burgers’ equation, which meets the Pressio FOM API as described below.

The purpose of defining this abstract FomSystem type is to indicate how Pressio is agnostic to the actual FOM implementation. As long as the FOM meets the required API, Pressio can work with it.

With that, all set-up is complete and we can move on to building and running the ROM.

Step 1: Define the Full-Order Model (FOM)

The first step in building a ROM is defining the full-order model (FOM) that we want to reduce.

In this tutorial, we will be working with the 1-D Burgers’ equation with periodic boundary conditions, as explained in the Background section.

Pressio expects a FOM to be defined such that it meets a specific API. There are a variety of different APIs depending on the type of problem being solved (e.g., steady vs unsteady, linear vs nonlinear, etc.). These APIs, defined as C++ concepts, can be found in the Pressio documentation.

We’ll use the API for a semi-discrete FOM defined by a system of ODEs:

\[\frac{d\mathbf{u}}{dt} = \mathbf{f}(\mathbf{u}, t; \mu)\]

where \(\mathbf{u}\) is the state vector, \(t\) is time, and \(\mu\) is a parameter.

According to the Pressio API for such a FOM, we need to define a class that meets the following API:

  • Three core types: time_type, state_type, and rhs_type

  • A method createRhs() that returns a new right-hand side vector

  • A method rhs(const state_type & u, time_type t, rhs_type & f) that computes the right-hand side

Note

“RHS” here stands for “right-hand side,” and refers to the function \(\mathbf{f}(\mathbf{u}, t; \mu)\) in the ODE above.

Step 2: Run the FOM to generate snapshots

In order to generate a reduced-order model, we need to collect snapshots of the FOM solution over time. The runFom function in the source code performs this task.

Typically, applications would implement this step (and the prior definition of the FOM) themselves. In this tutorial, we provide a simple implementation of a Forward Euler time integrator that advances the FOM in time and stores snapshots of both the state and the right-hand side at each time step.

for ( int step = 0; step < numSteps; ++step ) {
   stateSnaps.push_back( u );   // Store state snapshot
   auto f = fom.createRhs();    // Create RHS vector
   fom.rhs( u, t, f );          // Compute RHS
   rhsSnaps.push_back( f );     // Store RHS snapshot
   u += dt * f;                 // Update solution
   t += dt;                     // Advance time
 }

We store these snapshots in a SnapshotSet object that will be used later to construct the reduced basis via Proper Orthogonal Decomposition (POD).

Step 3: Build the ROM from the snapshot matrix

Note

The burgers.cpp driver includes code for building both standard and hyper-reduced ROMs. For now, we’ll focus on the standard ROM, and leave a discussion of hyper-reduction for the Hyper-Reduction tutorial that follows this one.

Before we can use Pressio to construct the ROM, we need to find the reduced basis from the collected snapshots. This is done using POD, which identifies the most energetic modes in the snapshot data.

Snapshots to Trial Space

In this tutorial, we compute the singular value decomposition (SVD) of the snapshot matrix and select the first r=5 modes to form the reduced basis. The selection of the number of modes is arbitrary–you may experiment with how changing the value of r affects the results. Heuristically, we find that r=5 typically captures most of the system’s energy.

Once we have the basis, we can use Pressio’s create_trial_column_subspace function to create a trial subspace object that represents the reduced basis:

auto trialSpace = pressio::rom::create_trial_column_subspace<vector_t>(
   basis, affineShift, isAffine
);

Here, basis is a matrix whose columns are the selected POD modes, and affineShift is the mean of the snapshots. The isAffine argument indicates whether we are using an affine trial space.

This is implemented in the helpers.h file by the snapshots_to_trial_space function.

Trial Space to Reduced State

Next, we can define the reduced state by projecting the FOM’s initial condition onto the reduced basis. To do this, we use Pressio to compute the reduced state vector:

auto reducedState = trialSpace.createReducedState();

// reducedState = basis^T * (u0 - affineShift)
reducedState = trialSpace.basisOfTranslatedSpace().transpose() *
               (fomInitialState - trialSpace.translationVector());

This is implemented in the helpers.h file by the trial_space_to_reduced_state function.

Determine the Time Integration Scheme

Since the FOM is typically defined by an application independently of Pressio, we set up our own Forward Euler time integrator in Step 2. However, now that we’re using Pressio to build and run the ROM, we can use Pressio’s built-in time integrators.

The stepScheme defined below will be passed to the ROM constructor and used when we advance the ROM in time.

auto stepScheme = pressio::ode::StepScheme::ForwardEuler;

The step schemes (both implicit and explicit) that are supported by Pressio can be found in the pressio-rom/ode module here.

Build the ROM

With the trial space, reduced state, and time integrator defined, we can now construct the reduced-order model using Pressio’s built-in functions.

We’ll start with a default Galerkin ROM, which projects the FOM’s dynamics onto the trial space using a Galerkin projection. Later on, we’ll revisit this step to implement hyper-reduction.

auto rom = pressio::rom::galerkin::create_unsteady_explicit_problem(
   stepScheme,
   trialSpace,
   fom
);

Step 4: Run the ROM

With the ROM constructed, we can now advance it in time using Pressio’s ode module.

Define the Time Stepping Policy

We create a stepping policy using the same time step size dt and number of steps numSteps as we used for the FOM simulation:

auto policy = pressio::ode::steps_fixed_dt(
    startTime,
    pressio::ode::StepCount( numSteps ),
    dt
);

There are other ways to define stepping policies in Pressio, such as using the start time, end time, and step size instead of the number of steps. These options are documented in the ode module.

Optional: Define the Observer

This step is optional, but it will allow us to monitor the ROM’s progress as it advances in time. Since we want to plot the results at the end, we’ll define an Observer that collects the ROM state at each time step.

Observers in Pressio are user-defined functors that are called at each time step during the time integration process. They can be used to log data, compute quantities of interest, or store states for later analysis.

Observers must meet the API defined by Pressio. Namely, they must implement the operator() method that takes the current time, step number, and state as arguments.

Our RomObserver will create the full state at every time step and store it in a trajectory member variable for later.

// Inside of RomObserver struct...
template <typename StepCountType, typename TimeType, typename StateType>
void operator()(StepCountType step, TimeType /*time*/, const StateType& reducedState) const
{
    // Capture every timestep
    auto fullState = trialSpace.createFullStateFromReducedState( reducedState );
    trajectory.push_back( fullState );
    ++stepCount;
}

The full RomObserver struct is implemented in the helpers.h file.

Advance the ROM in Time

Finally, we can advance the ROM in time using Pressio’s advance function:

pressio::ode::advance( rom, reducedState, policy, observer );

If you defined an observer, it will be called at each time step. Otherwise, simply omit the observer argument.

Reconstruct the Full-Order Solution

After the ROM has been advanced in time, the reducedState we defined earlier contains the final reduced state at the end of the simulation.

To get the solution in the full-order space, we need to reconstruct it using the trial space:

auto romSolution = trialSpace.createFullStateFromReducedState( reducedState );

Step 5: Compare ROM and FOM Solutions

At this point, we have constructed and run our reduced-order model. The remaining steps will compare the results to the FOM and plot the outcomes.

We already have the ROM solution at the final timestep, reconstructed back to the full-order space. Now, to get the FOM solution, we just extract the last snapshot from the FOM SnapshotSet we collected earlier. Specifically, we want the last column of the state snapshot matrix:

auto fomSolution = snapshots.stateSnapshots.col( snapshots.stateSnapshots.cols() - 1 );

With both the FOM and ROM solutions available, we can now compare them to assess the accuracy of the reduced-order model. We compute the L2 norm of the difference between the two solutions:

double error = ( fomSolution - romSolution ).norm() / fomSolution.norm();

This relative error gives us a measure of how well the ROM approximates the FOM solution. You should see something like:

[info] Relative error between FOM and default ROM: 0.0026138604529909602

In other words, the ROM solution is within about 0.26% of the FOM solution, which is quite accurate given that we only used 5 POD modes. Plus, for larger systems, the computational savings from using a ROM can be significant.

Step 6: Analyze the Results

Our Observer in Step 4 collected the ROM states at each time step and stored them in the trajectory vector. We can write these states to CSV files for further analysis and plotting.

After running the main executable:

cd $BUILD/full-tutorial
./burgers

we can generate plots with the output by running the python script in the same directory. It is hard-coded to find the output files in the output/ subdirectory.

cd $BUILD/full-tutorial
python plot.py

The resulting plots show how the ROM solutions compare to the FOM at various points in the simulation. You should see that the ROM closely tracks the FOM solution throughout the time integration.

The plot will be saved to your current directory as burgers_rom_comparison.png.

Note

You’ll find that the hyper-reduced ROM does not perform quite as well as the standard ROM. But the tradeoff is that the hyper-reduced ROM is computationally cheaper to run, especially for large-scale systems.

Go to top

Full Source File

  1///////////////////////////////////////////////////////////////////////////////
  2// Step 0: Pressio Setup
  3//////////////////////////////////////////////////////////////////////////////////
  4
  5/**
  6 * Pressio uses macros to enable features like logging and TPLs.
  7 * Typically, these would be set during configuration, but we will
  8 * define them explicitly here for demonstration purposes.
  9 *
 10 * Importantly, these macros should be defined BEFORE including any Pressio
 11 * headers.
 12 */
 13#define PRESSIO_ENABLE_LOGGING
 14#define PRESSIO_ENABLE_TPL_EIGEN
 15
 16/**
 17 * Due to the hierarchical structure of Pressio, including
 18 * pressio/rom.hpp will also pull in all necessary dependencies
 19 * (including Eigen, since we defined the macro above).
 20 * The logging macros (from pressio-log) are also included here.
 21 */
 22#include <pressio/rom.hpp>
 23
 24/**
 25 * We'll also need some helper functions along the way.
 26 */
 27#include "helpers.h"
 28
 29/**
 30 * And some functions for hyper-reduction.
 31 */
 32#include "hyperreduction.h"
 33
 34/**
 35 * And we'll use std::vectors to store snapshots.
 36 */
 37#include <vector>
 38
 39/**
 40 * Pressio supports various linear algebra backends (such as
 41 * Eigen, Tpetra, and Kokkos) and provides a unified interface
 42 * with the pressio::ops library for common operations. In this
 43 * tutorial, we will use Eigen.
 44 */
 45using vector_t = Eigen::VectorXd;
 46using matrix_t = Eigen::MatrixXd;
 47
 48///////////////////////////////////////////////////////////////////////////////
 49// Step 1: Define the Full-Order Model (FOM)
 50///////////////////////////////////////////////////////////////////////////////
 51
 52/**
 53 * Pressio requires a specific API to be used for the FOM class. This API can
 54 * vary depending on the type of problem. For the 1D Burgers' equation,
 55 * we will use the API for a semi-discrete FOM for time-dependent problems of
 56 * the form:
 57 *
 58 *     d/dt y(t) = f(y, t).
 59 *
 60 * The various FOM APIs can be found in pressio/rom/concepts.hpp.
 61 *
 62 * According to the semi-discrete API, our FOM class must define the following:
 63 *
 64 *     1. Three core types
 65 *         - time_type, state_type, rhs_type
 66 *
 67 *     2. A method to create the RHS vector
 68 *         - createRhs() -> rhs_type
 69 *
 70 *     3. A method to compute the RHS
 71 *         - rhs(const state_type & u, time_type t, rhs_type & f) -> void
 72 *
 73 * And that's it!
 74 */
 75class Burgers1dFom
 76{
 77    public:
 78        // API Requirement: Core types
 79        using time_type  = double;
 80        using state_type = vector_t;
 81        using rhs_type   = vector_t;
 82
 83    private:
 84        std::size_t N_{ };
 85        double dx_{ };
 86        double nu_{ };
 87
 88    public:
 89        Burgers1dFom( std::size_t N, double domainLength, double viscosity )
 90        : N_( N ), dx_( domainLength / ( N ) ), nu_( viscosity )
 91        {
 92            assert( N_  >= 3 );
 93            assert( dx_ > 0.0 );
 94            assert( nu_ > 0.0 );
 95        }
 96
 97        std::size_t N() const { return N_; }
 98        double dx()     const { return dx_; }
 99        double nu()     const { return nu_; }
100
101        // API Requirement: createRhs() method
102        rhs_type createRhs() const
103        {
104            return rhs_type::Zero( N_ );
105        }
106
107        // API Requirement: rhs(...) method
108        void rhs(
109            const state_type & u,
110            time_type /*t*/,
111            rhs_type & f
112        ) const
113        {
114            assert( u.size() == N_ );
115            assert( f.size() == N_ );
116
117            // periodic indexing helpers
118            auto ip = [ this ]( std::size_t i ){ return ( i + 1 ) % N_; };
119            auto im = [ this ]( std::size_t i ){ return ( i + N_ - 1 ) % N_; };
120
121            const double inv2dx = 1.0 / ( 2.0 * dx_ );
122            const double invdx2 = 1.0 / ( dx_ * dx_ );
123
124            for ( std::size_t i = 0; i < N_; ++i ) {
125                const std::size_t iL = im( i );
126                const std::size_t iR = ip( i );
127
128                // convective term: -(1/2 d/dx (u^2)) using centered flux difference
129                // d/dx (u^2) ~ (u_{i+1}^2 - u_{i-1}^2) / (2 dx)
130                const double dudt_conv =
131                    -0.5 * ( ( u[ iR ] * u[ iR ] ) - ( u[ iL ] * u[ iL ] ) ) * inv2dx;
132
133                // viscous term: nu * u_xx using second-order centered FD
134                const double dudt_diff =
135                    nu_ * ( u[ iR ] - 2.0 * u[ i ] + u[ iL ] ) * invdx2;
136                f[ i ] = dudt_conv + dudt_diff;
137            }
138        }
139};
140
141///////////////////////////////////////////////////////////////////////////////
142// Step 2: Run the FOM to generate snapshots (states and RHS)
143////////////////////////////////////////////////////////////////////////////////
144
145struct SnapshotSet
146{
147    matrix_t stateSnapshots;
148    matrix_t rhsSnapshots;
149};
150
151/**
152 * Building and running the FOM doesn't require any Pressio utilities.
153 * For this example, we'll define a simple Forward Euler time integrator
154 * to advance the FOM in time and collect the snapshots that will be used
155 * to build the ROM with Pressio.
156 */
157template <typename FomSystem>
158SnapshotSet runFOM( FomSystem& fom, typename FomSystem::time_type startTime, int numSteps, double dt )
159{
160    // We will store each snapshot vector in a std::vector for now
161    // For a typical ROM, we would only need the state snapshots. However,
162    // for hyper-reduction, we will also need snapshots of the RHS.
163    std::vector< vector_t > stateSnaps;
164    std::vector< vector_t > rhsSnaps;
165
166    // Compute the initial condition
167    auto u = computeInitialCondition< FomSystem, vector_t >( fom );
168
169    // Time integration loop (using Forward Euler)
170    double t = startTime;
171    for ( int step = 0; step < numSteps; ++step ) {
172        stateSnaps.push_back( u );   // Store state snapshot
173        auto f = fom.createRhs();    // Create RHS vector
174        fom.rhs( u, t, f );          // Compute RHS
175        rhsSnaps.push_back( f );     // Store RHS snapshot
176        u += dt * f;                 // Update solution
177        t += dt;                     // Advance time
178    }
179
180    // Convert our vector of snapshot vectors into matrices
181    const std::size_t numSnapshots = stateSnaps.size();
182    matrix_t stateMatrix( fom.N(), numSnapshots );
183    matrix_t rhsMatrix( fom.N(), numSnapshots );
184    for ( std::size_t i = 0; i < numSnapshots; ++i ) {
185        stateMatrix.col( i ) = stateSnaps[ i ];
186        rhsMatrix.col( i )      = rhsSnaps[ i ];
187    }
188
189    // Return both snapshot matrices
190    return SnapshotSet{ std::move(stateMatrix), std::move(rhsMatrix) };
191}
192
193///////////////////////////////////////////////////////////////////////////////
194// Step 3: Build the ROM from the snapshot matrix
195///////////////////////////////////////////////////////////////////////////////
196
197/**
198 * Now we use the Pressio ecosystem to construct a ROM representation
199 * with the snapshot matrix. As before, there are various APIs that we can meet
200 * to use different types of ROMs. We'll use a basic Galerkin ROM here.
201 */
202
203template <typename FomSystem>
204auto buildStandardGalerkinROM( FomSystem& fom, auto& stepScheme, auto& trialSpace )
205{
206    return pressio::rom::galerkin::create_unsteady_explicit_problem(
207        stepScheme, trialSpace, fom
208    );
209}
210
211/**
212 * Hyper-reduced ROMs do not evaluate the full RHS at every time step.
213 * Instead, they sample the RHS at selected indices and use a
214 * projection to approximate the reduced RHS. This can greatly
215 * reduce the computational cost of evaluating the ROM, especially
216 * when the FOM is large and the RHS evaluation is expensive.
217 *
218 * However, there are trade-offs in accuracy, as you'll see.
219 */
220template <typename FomSystem>
221auto buildHyperReducedGalerkinROM( FomSystem& fom, auto& stepScheme,
222                                   auto& trialSpace, auto& hyperReducer )
223{
224    // Build the hyperreduced Galerkin ROM by passing the hyper-reducer
225    // functor to the ROM factory function.
226    return pressio::rom::galerkin::create_unsteady_explicit_problem(
227        stepScheme, trialSpace, fom, hyperReducer
228    );
229}
230
231///////////////////////////////////////////////////////////////////////////////
232// Step 4: Run the ROM with trajectory capture
233///////////////////////////////////////////////////////////////////////////////
234/**
235 * To run the ROM, we will define a simple observer that captures
236 * the reduced state at each time step and reconstructs the full-order
237 * state using the trial space. This allows us to capture the full
238 * trajectory of the ROM solution for later analysis.
239 */
240template <typename FomSystem>
241auto runROM( auto& rom, auto& trialSpace, auto& reducedState,
242             typename FomSystem::time_type startTime, int numSteps, double dt,
243             std::vector<vector_t>& trajectory
244) {
245    // Define the ODE time stepping policy
246    auto policy = pressio::ode::steps_fixed_dt(
247        startTime,
248        pressio::ode::StepCount( numSteps ),
249        dt
250    );
251
252    /**
253     * In Pressio, observers are functors that are called at each time step
254     * during time integration. Here, we define an observer that captures
255     * the full-order state at each time step by reconstructing it from
256     * the reduced state using the trial space.
257     *
258     * Observers must meet a specific API defined by Pressio. Namely, they
259     * must implement the operator() with the signature shown in helpers.h.
260     */
261    auto observer = RomObserver< vector_t, decltype(trialSpace) >( trajectory, trialSpace );
262
263    // Run the ROM time integration with observer
264    pressio::ode::advance( rom, reducedState, policy, observer );
265
266    /**
267     * At this point, reducedState contains the ROM solution at the
268     * final timestep. We just have to reconstruct the full-order
269     * solution from the time reduced coordinates via the trial subspace.
270     */
271    auto romSolution = trialSpace.createFullStateFromReducedState( reducedState );
272
273    return romSolution;
274}
275
276///////////////////////////////////////////////////////////////////////////////
277// Step 5: Compare FOM and ROM solutions
278///////////////////////////////////////////////////////////////////////////////
279
280/**
281 * To compare the FOM and ROM solutions, we can compute the relative error
282 * between the two solution vectors at the final timestep.
283 */
284double compareFomAndRom( const matrix_t& fomSolution, const matrix_t& romSolution )
285{
286    double error = ( fomSolution - romSolution ).norm() / fomSolution.norm();
287    return error;
288}
289
290///////////////////////////////////////////////////////////////////////////////
291// Main driver
292///////////////////////////////////////////////////////////////////////////////
293
294int main() {
295    // Initialize the logger so we can see Pressio output
296    // Change the log level to "debug" for more information
297    PRESSIOLOG_INITIALIZE(pressiolog::LogLevel::info);
298
299    using FomSystem = Burgers1dFom;
300    using time_t = typename FomSystem::time_type;
301
302    // 1. Create the FOM
303    const std::size_t N = 100;          // Number of spatial points
304    const double domainLength = 1.0;    // Domain length
305    const double viscosity = 0.01;      // Viscosity parameter
306    FomSystem fom( N, domainLength, viscosity );
307    PRESSIOLOG_INFO( "1. Created FOM with N = {}, dx = {}, nu = {}", fom.N(), fom.dx(), fom.nu() );
308
309    // 2. Run the FOM to generate a snapshot matrix
310    const double dt    = 0.001;
311    const int numSteps = 1000;
312    time_t startTime   = 0.0;
313    SnapshotSet snapshots = runFOM< FomSystem >( fom, startTime, numSteps, dt );
314    PRESSIOLOG_INFO( "2. Generated snapshot matrix with {} snapshots", snapshots.stateSnapshots.cols() );
315
316    // 3 and 4. Build and run the ROM(s)
317
318    /**
319     * This tutorial covers both standard and hyper-reduced ROMs.
320     * These variables below will hold the trajectories and solutions,
321     * which we can analyze at the end.
322     */
323    std::vector<vector_t> standardRomTrajectory;
324    std::vector<vector_t> hypredRomTrajectory;
325    matrix_t standardSolution;
326    matrix_t hypredSolution;
327
328    // Start with the standard ROM
329    PRESSIOLOG_INFO( "Standard ROM:" );
330    {
331        // 3. Build the ROM
332
333        // Build the state trial space (Phi) from state snapshots
334        auto standardTrialSpace = snapshots_to_trial_space< FomSystem, matrix_t, vector_t >( snapshots.stateSnapshots, fom );
335        // Create the initial reduced state by projecting the FOM initial condition
336        auto standardReducedState = trial_space_to_reduced_state< FomSystem, vector_t >( standardTrialSpace, fom );
337        // Select the time integration scheme with Pressio
338        auto stepScheme = pressio::ode::StepScheme::ForwardEuler;
339        // Build the standard ROM
340        auto standardRom = buildStandardGalerkinROM< FomSystem >( fom, stepScheme, standardTrialSpace );
341        PRESSIOLOG_INFO( "  3. Built standard Galerkin ROM" );
342
343        // 4. Run the ROM and capture the trajectory
344        standardSolution = runROM< FomSystem >( standardRom, standardTrialSpace, standardReducedState, startTime, numSteps, dt, standardRomTrajectory );
345        PRESSIOLOG_INFO( "  4. Ran standard ROM and captured trajectory" );
346    }
347
348    // Follow a similar process for hyper-reduced ROM
349    PRESSIOLOG_INFO( "Hyper-reduced ROM:" );
350    {
351        // 3. Build the ROM
352        auto hypredTrialSpace   = snapshots_to_trial_space< FomSystem, matrix_t, vector_t >( snapshots.stateSnapshots, fom );
353        auto hypredReducedState = trial_space_to_reduced_state< FomSystem, vector_t >( hypredTrialSpace, fom );
354        auto stepScheme = pressio::ode::StepScheme::ForwardEuler;
355        /**
356         * Here, we build the hyper-reducer using the RHS snapshot matrix
357         * and the trial space. The hyper-reducer is a functor that will
358         * be used by the ROM to compute the reduced RHS from sampled FOM RHSs.
359         */
360        auto hyperReducer = buildHyperReducer< vector_t, matrix_t >( snapshots.rhsSnapshots, hypredTrialSpace );
361        auto hypredRom    = buildHyperReducedGalerkinROM< FomSystem >( fom, stepScheme, hypredTrialSpace, hyperReducer );
362        PRESSIOLOG_INFO( "  3. Built hyper-reduced Galerkin ROM" );
363
364        // 4. Run the ROM and capture the trajectory
365        hypredSolution = runROM< FomSystem >( hypredRom, hypredTrialSpace, hypredReducedState, startTime, numSteps, dt, hypredRomTrajectory );
366        PRESSIOLOG_INFO( "  4. Ran hyper-reduced ROM and captured trajectory" );
367    }
368
369    // 5. Compare ROM solution against FOM solution (the last snapshot)
370    auto fomSolution = snapshots.stateSnapshots.col( snapshots.stateSnapshots.cols() - 1 );
371    auto romError = compareFomAndRom( fomSolution, standardSolution );
372    PRESSIOLOG_INFO( "5. Relative error between FOM and standard ROM: {}", romError );
373    auto hypredError = compareFomAndRom( fomSolution, hypredSolution );
374    PRESSIOLOG_INFO( "   Relative error between FOM and hyper-reduced ROM: {}", hypredError );
375
376    // 6. Write trajectories to CSV files in output directory
377    // Get FOM trajectory from snapshots
378    std::vector<vector_t> fomTrajectory;
379    for (int i = 0; i < snapshots.stateSnapshots.cols(); ++i) {
380        fomTrajectory.push_back(snapshots.stateSnapshots.col(i));
381    }
382    writeTrajectoryToCSV< vector_t >("output", "fom_trajectory.csv",         fomTrajectory);
383    writeTrajectoryToCSV< vector_t >("output", "standard_rom_trajectory.csv", standardRomTrajectory);
384    writeTrajectoryToCSV< vector_t >("output", "hypred_rom_trajectory.csv",  hypredRomTrajectory);
385    PRESSIOLOG_INFO( "6. Wrote trajectories to output/{fom,default_rom,hypred_rom}_trajectory.csv" );
386
387    // Finalize the logger
388    PRESSIOLOG_FINALIZE();
389    return 0;
390}

Go to top