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:
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, andrhs_typeA method
createRhs()that returns a new right-hand side vectorA 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.
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}