LCOV - code coverage report
Current view: top level - drivers - drivensolver.cpp (source / functions) Coverage Total Hit
Test: Palace Coverage Report Lines: 0.0 % 190 0
Test Date: 2025-10-23 22:45:05 Functions: 0.0 % 4 0
Legend: Lines: hit not hit

            Line data    Source code
       1              : // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
       2              : // SPDX-License-Identifier: Apache-2.0
       3              : 
       4              : #include "drivensolver.hpp"
       5              : 
       6              : #include <complex>
       7              : #include <mfem.hpp>
       8              : #include "fem/errorindicator.hpp"
       9              : #include "fem/mesh.hpp"
      10              : #include "linalg/errorestimator.hpp"
      11              : #include "linalg/floquetcorrection.hpp"
      12              : #include "linalg/ksp.hpp"
      13              : #include "linalg/operator.hpp"
      14              : #include "linalg/vector.hpp"
      15              : #include "models/lumpedportoperator.hpp"
      16              : #include "models/portexcitations.hpp"
      17              : #include "models/postoperator.hpp"
      18              : #include "models/romoperator.hpp"
      19              : #include "models/spaceoperator.hpp"
      20              : #include "models/surfacecurrentoperator.hpp"
      21              : #include "models/waveportoperator.hpp"
      22              : #include "utils/communication.hpp"
      23              : #include "utils/iodata.hpp"
      24              : #include "utils/prettyprint.hpp"
      25              : #include "utils/timer.hpp"
      26              : 
      27              : namespace palace
      28              : {
      29              : 
      30              : using namespace std::complex_literals;
      31              : 
      32              : std::pair<ErrorIndicator, long long int>
      33            0 : DrivenSolver::Solve(const std::vector<std::unique_ptr<Mesh>> &mesh) const
      34              : {
      35              :   // Set up the spatial discretization and frequency sweep.
      36            0 :   BlockTimer bt0(Timer::CONSTRUCT);
      37            0 :   SpaceOperator space_op(iodata, mesh);
      38              :   const auto &port_excitations = space_op.GetPortExcitations();
      39            0 :   SaveMetadata(port_excitations);
      40              : 
      41            0 :   const auto &omega_sample = iodata.solver.driven.sample_f;
      42              : 
      43            0 :   bool adaptive = (iodata.solver.driven.adaptive_tol > 0.0);
      44            0 :   if (adaptive && omega_sample.size() <= iodata.solver.driven.prom_indices.size())
      45              :   {
      46            0 :     Mpi::Warning("Adaptive frequency sweep requires > {} total frequency samples!\n"
      47              :                  "Reverting to uniform sweep!\n",
      48            0 :                  iodata.solver.driven.prom_indices.size());
      49              :     adaptive = false;
      50              :   }
      51            0 :   SaveMetadata(space_op.GetNDSpaces());
      52            0 :   Mpi::Print("\nComputing {}frequency response for:\n{}", adaptive ? "adaptive fast " : "",
      53            0 :              port_excitations.FmtLog());
      54              : 
      55            0 :   auto restart = iodata.solver.driven.restart;
      56            0 :   if (restart != 1)
      57              :   {
      58            0 :     int max_iter = omega_sample.size() * space_op.GetPortExcitations().Size();
      59            0 :     MFEM_VERIFY(
      60              :         restart - 1 < max_iter,
      61              :         fmt::format("\"Restart\" ({}) is greater than the number of total samples ({})!",
      62              :                     restart, max_iter));
      63              : 
      64            0 :     Mpi::Print("\nRestarting from solve {}", iodata.solver.driven.restart);
      65              :   }
      66              : 
      67              :   // Main frequency sweep loop.
      68            0 :   return {adaptive ? SweepAdaptive(space_op) : SweepUniform(space_op),
      69            0 :           space_op.GlobalTrueVSize()};
      70            0 : }
      71              : 
      72            0 : ErrorIndicator DrivenSolver::SweepUniform(SpaceOperator &space_op) const
      73              : {
      74              :   const auto &port_excitations = space_op.GetPortExcitations();
      75            0 :   const auto &omega_sample = iodata.solver.driven.sample_f;
      76              : 
      77              :   // Initialize postprocessing for measurement and printers.
      78              :   // Initialize write directory with default path; will be changed for multi-excitations.
      79            0 :   PostOperator<ProblemType::DRIVEN> post_op(iodata, space_op);
      80              : 
      81              :   // Construct the system matrices defining the linear operator. PEC boundaries are handled
      82              :   // simply by setting diagonal entries of the system matrix for the corresponding dofs.
      83              :   // Because the Dirichlet BC is always homogeneous, no special elimination is required on
      84              :   // the RHS. Assemble the linear system for the initial frequency (so we can call
      85              :   // KspSolver::SetOperators). Compute everything at the first frequency step.
      86            0 :   auto K = space_op.GetStiffnessMatrix<ComplexOperator>(Operator::DIAG_ONE);
      87            0 :   auto C = space_op.GetDampingMatrix<ComplexOperator>(Operator::DIAG_ZERO);
      88            0 :   auto M = space_op.GetMassMatrix<ComplexOperator>(Operator::DIAG_ZERO);
      89              :   const auto &Curl = space_op.GetCurlMatrix();
      90              : 
      91              :   // Set up the linear solver.
      92              :   // The operators are constructed for each frequency step and used to initialize the ksp.
      93            0 :   ComplexKspSolver ksp(iodata, space_op.GetNDSpaces(), &space_op.GetH1Spaces());
      94              : 
      95              :   // Set up RHS vector for the incident field at port boundaries, and the vector for the
      96              :   // first frequency step.
      97            0 :   ComplexVector RHS(Curl.Width()), E(Curl.Width()), B(Curl.Height());
      98            0 :   RHS.UseDevice(true);
      99            0 :   E.UseDevice(true);
     100            0 :   B.UseDevice(true);
     101              :   E = 0.0;
     102              :   B = 0.0;
     103              : 
     104              :   // Initialize structures for storing and reducing the results of error estimation.
     105              :   TimeDependentFluxErrorEstimator<ComplexVector> estimator(
     106              :       space_op.GetMaterialOp(), space_op.GetNDSpaces(), space_op.GetRTSpaces(),
     107            0 :       iodata.solver.linear.estimator_tol, iodata.solver.linear.estimator_max_it, 0,
     108            0 :       iodata.solver.linear.estimator_mg);
     109              :   ErrorIndicator indicator;
     110              : 
     111              :   // If using Floquet BCs, a correction term (kp x E) needs to be added to the B field.
     112            0 :   std::unique_ptr<FloquetCorrSolver<ComplexVector>> floquet_corr;
     113            0 :   if (space_op.GetMaterialOp().HasWaveVector())
     114              :   {
     115            0 :     floquet_corr = std::make_unique<FloquetCorrSolver<ComplexVector>>(
     116              :         space_op.GetMaterialOp(), space_op.GetNDSpace(), space_op.GetRTSpace(),
     117            0 :         iodata.solver.linear.tol, iodata.solver.linear.max_it, 0);
     118              :   }
     119              : 
     120              :   // Main excitation and frequency loop.
     121              :   auto t0 = Timer::Now();
     122            0 :   int excitation_counter = 0;
     123              :   const int excitation_restart_counter =
     124            0 :       ((iodata.solver.driven.restart - 1) / omega_sample.size()) + 1;
     125            0 :   const int freq_restart_idx = (iodata.solver.driven.restart - 1) % omega_sample.size();
     126            0 :   for (const auto &[excitation_idx, excitation_spec] : port_excitations)
     127              :   {
     128            0 :     if (++excitation_counter < excitation_restart_counter)
     129              :     {
     130            0 :       continue;
     131              :     }
     132            0 :     if (port_excitations.Size() > 1)
     133              :     {
     134            0 :       Mpi::Print("\nSweeping excitation index {:d} ({:d}/{:d}):\n", excitation_idx,
     135            0 :                  excitation_counter, port_excitations.Size());
     136              :     }
     137              :     // Switch paraview subfolders: one for each excitation, if nr_excitations > 1.
     138            0 :     post_op.InitializeParaviewDataCollection(excitation_idx);
     139              : 
     140              :     // Frequency loop.
     141              :     for (std::size_t omega_i =
     142            0 :              ((excitation_counter == excitation_restart_counter) ? freq_restart_idx : 0);
     143            0 :          omega_i < omega_sample.size(); omega_i++)
     144              :     {
     145            0 :       auto omega = omega_sample[omega_i];
     146              :       // Assemble frequency dependent matrices and initialize operators in linear
     147              :       // solver.
     148            0 :       auto A2 = space_op.GetExtraSystemMatrix<ComplexOperator>(omega, Operator::DIAG_ZERO);
     149            0 :       auto A = space_op.GetSystemMatrix(1.0 + 0.0i, 1i * omega, -omega * omega + 0.0i,
     150            0 :                                         K.get(), C.get(), M.get(), A2.get());
     151              :       auto P = space_op.GetPreconditionerMatrix<ComplexOperator>(
     152            0 :           1.0 + 0.0i, 1i * omega, -omega * omega + 0.0i, omega);
     153            0 :       ksp.SetOperators(*A, *P);
     154              : 
     155            0 :       Mpi::Print(
     156              :           "\nIt {:d}/{:d}: ω/2π = {:.3e} GHz (total elapsed time = {:.2e} s{})\n",
     157            0 :           omega_i + 1, omega_sample.size(),
     158            0 :           iodata.units.Dimensionalize<Units::ValueType::FREQUENCY>(omega),
     159            0 :           Timer::Duration(Timer::Now() - t0).count(),
     160              :           (port_excitations.Size() > 1)
     161            0 :               ? fmt::format(", solve {:d}/{:d}",
     162            0 :                             1 + omega_i + (excitation_counter - 1) * omega_sample.size(),
     163            0 :                             omega_sample.size() * port_excitations.Size())
     164            0 :               : "");
     165              : 
     166              :       // Solve linear system.
     167            0 :       space_op.GetExcitationVector(excitation_idx, omega, RHS);
     168            0 :       Mpi::Print("\n");
     169            0 :       ksp.Mult(RHS, E);
     170              : 
     171              :       // Start Post-processing.
     172            0 :       BlockTimer bt0(Timer::POSTPRO);
     173            0 :       Mpi::Print(" Sol. ||E|| = {:.6e} (||RHS|| = {:.6e})\n",
     174            0 :                  linalg::Norml2(space_op.GetComm(), E),
     175            0 :                  linalg::Norml2(space_op.GetComm(), RHS));
     176              : 
     177              :       // Compute B = -1/(iω) ∇ x E on the true dofs.
     178            0 :       Curl.Mult(E.Real(), B.Real());
     179            0 :       Curl.Mult(E.Imag(), B.Imag());
     180            0 :       B *= -1.0 / (1i * omega);
     181            0 :       if (space_op.GetMaterialOp().HasWaveVector())
     182              :       {
     183              :         // Calculate B field correction for Floquet BCs.
     184              :         // B = -1/(iω) ∇ x E + 1/ω kp x E
     185            0 :         floquet_corr->AddMult(E, B, 1.0 / omega);
     186              :       }
     187              : 
     188              :       auto total_domain_energy =
     189            0 :           post_op.MeasureAndPrintAll(excitation_idx, int(omega_i), E, B, omega);
     190              : 
     191              :       // Calculate and record the error indicators.
     192            0 :       Mpi::Print(" Updating solution error estimates\n");
     193            0 :       estimator.AddErrorIndicator(E, B, total_domain_energy, indicator);
     194            0 :     }
     195              : 
     196              :     // Final postprocessing & printing.
     197            0 :     BlockTimer bt0(Timer::POSTPRO);
     198            0 :     SaveMetadata(ksp);
     199            0 :   }
     200            0 :   post_op.MeasureFinalize(indicator);
     201            0 :   return indicator;
     202            0 : }
     203              : 
     204            0 : ErrorIndicator DrivenSolver::SweepAdaptive(SpaceOperator &space_op) const
     205              : {
     206              :   const auto &port_excitations = space_op.GetPortExcitations();
     207            0 :   const auto &omega_sample = iodata.solver.driven.sample_f;
     208              : 
     209              :   // Initialize postprocessing for measurement and printers.
     210              :   // Initialize write directory with default path; will be changed for multi-excitations.
     211            0 :   PostOperator<ProblemType::DRIVEN> post_op(iodata, space_op);
     212              : 
     213              :   // Configure PROM parameters if not specified.
     214            0 :   double offline_tol = iodata.solver.driven.adaptive_tol;
     215            0 :   int convergence_memory = iodata.solver.driven.adaptive_memory;
     216            0 :   int max_size_per_excitation = iodata.solver.driven.adaptive_max_size;
     217            0 :   int nprom_indices = static_cast<int>(iodata.solver.driven.prom_indices.size());
     218            0 :   MFEM_VERIFY(max_size_per_excitation <= 0 || max_size_per_excitation >= nprom_indices,
     219              :               "Adaptive frequency sweep must sample at least " << nprom_indices
     220              :                                                                << " frequency points!");
     221              :   // Maximum size — no more than nr steps needed.
     222            0 :   max_size_per_excitation =
     223            0 :       std::min(max_size_per_excitation, static_cast<int>(omega_sample.size()));
     224              : 
     225              :   // Allocate negative curl matrix for postprocessing the B-field and vectors for the
     226              :   // high-dimensional field solution.
     227              :   const auto &Curl = space_op.GetCurlMatrix();
     228            0 :   ComplexVector E(Curl.Width()), Eh(Curl.Width()), B(Curl.Height());
     229            0 :   E.UseDevice(true);
     230            0 :   Eh.UseDevice(true);
     231            0 :   B.UseDevice(true);
     232              :   E = 0.0;
     233              :   Eh = 0.0;
     234              :   B = 0.0;
     235              : 
     236              :   // Initialize structures for storing and reducing the results of error estimation.
     237              :   TimeDependentFluxErrorEstimator<ComplexVector> estimator(
     238              :       space_op.GetMaterialOp(), space_op.GetNDSpaces(), space_op.GetRTSpaces(),
     239            0 :       iodata.solver.linear.estimator_tol, iodata.solver.linear.estimator_max_it, 0,
     240            0 :       iodata.solver.linear.estimator_mg);
     241              :   ErrorIndicator indicator;
     242              : 
     243              :   // If using Floquet BCs, a correction term (kp x E) needs to be added to the B field.
     244            0 :   std::unique_ptr<FloquetCorrSolver<ComplexVector>> floquet_corr;
     245            0 :   if (space_op.GetMaterialOp().HasWaveVector())
     246              :   {
     247            0 :     floquet_corr = std::make_unique<FloquetCorrSolver<ComplexVector>>(
     248              :         space_op.GetMaterialOp(), space_op.GetNDSpace(), space_op.GetRTSpace(),
     249            0 :         iodata.solver.linear.tol, iodata.solver.linear.max_it, 0);
     250              :   }
     251              : 
     252              :   // Configure the PROM operator which performs the parameter space sampling and basis
     253              :   // construction during the offline phase as well as the PROM solution during the online
     254              :   // phase.
     255              :   auto t0 = Timer::Now();
     256            0 :   const double unit_GHz = iodata.units.Dimensionalize<Units::ValueType::FREQUENCY>(1.0);
     257            0 :   Mpi::Print("\nBeginning PROM construction offline phase:\n"
     258              :              " {:d} points for frequency sweep over [{:.3e}, {:.3e}] GHz\n",
     259            0 :              omega_sample.size(), omega_sample.front() * unit_GHz,
     260            0 :              omega_sample.back() * unit_GHz);
     261            0 :   RomOperator prom_op(iodata, space_op, max_size_per_excitation);
     262              :   space_op.GetWavePortOp().SetSuppressOutput(true);
     263              : 
     264              :   // Initialize the basis with samples from the top and bottom of the frequency
     265              :   // range of interest. Each call for an HDM solution adds the frequency sample to P_S and
     266              :   // removes it from P \ P_S. Timing for the HDM construction and solve is handled inside
     267              :   // of the RomOperator.
     268            0 :   auto UpdatePROM = [&](int excitation_idx, double omega)
     269              :   {
     270              :     // Add the HDM solution to the PROM reduced basis.
     271            0 :     prom_op.UpdatePROM(E);
     272            0 :     prom_op.UpdateMRI(excitation_idx, omega, E);
     273              : 
     274              :     // Compute B = -1/(iω) ∇ x E on the true dofs, and set the internal GridFunctions in
     275              :     // PostOperator for energy postprocessing and error estimation.
     276            0 :     BlockTimer bt0(Timer::POSTPRO);
     277            0 :     Curl.Mult(E.Real(), B.Real());
     278            0 :     Curl.Mult(E.Imag(), B.Imag());
     279            0 :     B *= -1.0 / (1i * omega);
     280            0 :     if (space_op.GetMaterialOp().HasWaveVector())
     281              :     {
     282              :       // Calculate B field correction for Floquet BCs.
     283              :       // B = -1/(iω) ∇ x E + 1/ω kp x E
     284            0 :       floquet_corr->AddMult(E, B, 1.0 / omega);
     285              :     }
     286              : 
     287              :     // Measure domain energies for the error indicator only. Don't exchange face_nbr_data,
     288              :     // unless printing paraview fields.
     289            0 :     auto total_domain_energy = post_op.MeasureDomainFieldEnergyOnly(E, B);
     290            0 :     estimator.AddErrorIndicator(E, B, total_domain_energy, indicator);
     291            0 :   };
     292              : 
     293              :   // Loop excitations to add to PROM.
     294              :   //
     295              :   // Restart should not really be used for adaptive sweeps, but must work. Construct PROM in
     296              :   // the same way same regardless of restart for consistency. Don't shift excitation start.
     297            0 :   int excitation_counter = 0;
     298            0 :   for (const auto &[excitation_idx, excitation_spec] : port_excitations)
     299              :   {
     300            0 :     if (port_excitations.Size() > 1)
     301              :     {
     302            0 :       Mpi::Print("\nAdding excitation index {:d} ({:d}/{:d}):\n", excitation_idx,
     303            0 :                  ++excitation_counter, port_excitations.Size());
     304              :     }
     305            0 :     prom_op.SetExcitationIndex(excitation_idx);  // Pre-compute RHS1
     306              : 
     307              :     // Initialize PROM with explicit HDM samples, record the estimate but do not act on it.
     308              :     std::vector<double> max_errors;
     309            0 :     for (auto i : iodata.solver.driven.prom_indices)
     310              :     {
     311            0 :       auto omega = omega_sample[i];
     312            0 :       prom_op.SolveHDM(excitation_idx, omega, E);
     313            0 :       prom_op.SolvePROM(excitation_idx, omega, Eh);
     314            0 :       linalg::AXPY(-1.0, E, Eh);
     315            0 :       max_errors.push_back(linalg::Norml2(space_op.GetComm(), Eh) /
     316            0 :                            linalg::Norml2(space_op.GetComm(), E));
     317            0 :       UpdatePROM(excitation_idx, omega);
     318              :     }
     319              :     // The estimates associated to the end points are assumed inaccurate.
     320            0 :     max_errors[0] = std::numeric_limits<double>::infinity();
     321            0 :     max_errors[1] = std::numeric_limits<double>::infinity();
     322              :     int memory = std::distance(max_errors.rbegin(),
     323            0 :                                std::find_if(max_errors.rbegin(), max_errors.rend(),
     324            0 :                                             [=](auto x) { return x > offline_tol; }));
     325              : 
     326              :     // Greedy procedure for basis construction (offline phase). Basis is initialized with
     327              :     // solutions at frequency sweep endpoints and explicit sample frequencies.
     328            0 :     int it = static_cast<int>(max_errors.size());
     329            0 :     for (int it0 = it; it < max_size_per_excitation && memory < convergence_memory; it++)
     330              :     {
     331              :       // Compute the location of the maximum error in parameter domain (bounded by the
     332              :       // previous samples).
     333            0 :       double omega_star = prom_op.FindMaxError(excitation_idx)[0];
     334              : 
     335              :       // Sample HDM and add solution to basis.
     336            0 :       prom_op.SolveHDM(excitation_idx, omega_star, E);
     337            0 :       prom_op.SolvePROM(excitation_idx, omega_star, Eh);
     338            0 :       linalg::AXPY(-1.0, E, Eh);
     339            0 :       max_errors.push_back(linalg::Norml2(space_op.GetComm(), Eh) /
     340            0 :                            linalg::Norml2(space_op.GetComm(), E));
     341            0 :       memory = max_errors.back() < offline_tol ? memory + 1 : 0;
     342              : 
     343            0 :       Mpi::Print("\nGreedy iteration {:d} (n = {:d}): ω* = {:.3e} GHz ({:.3e}), error = "
     344              :                  "{:.3e}{}\n",
     345            0 :                  it - it0 + 1, prom_op.GetReducedDimension(), omega_star * unit_GHz,
     346              :                  omega_star, max_errors.back(),
     347              :                  (memory == 0)
     348            0 :                      ? ""
     349            0 :                      : fmt::format(", memory = {:d}/{:d}", memory, convergence_memory));
     350            0 :       UpdatePROM(excitation_idx, omega_star);
     351              :     }
     352            0 :     Mpi::Print("\nAdaptive sampling{} {:d} frequency samples:\n"
     353              :                " n = {:d}, error = {:.3e}, tol = {:.3e}, memory = {:d}/{:d}\n",
     354            0 :                (it == max_size_per_excitation) ? " reached maximum" : " converged with", it,
     355            0 :                prom_op.GetReducedDimension(), max_errors.back(), offline_tol, memory,
     356              :                convergence_memory);
     357            0 :     utils::PrettyPrint(prom_op.GetSamplePoints(excitation_idx), unit_GHz,
     358              :                        " Sampled frequencies (GHz):");
     359            0 :     utils::PrettyPrint(max_errors, 1.0, " Sample errors:");
     360              :   }
     361              : 
     362            0 :   Mpi::Print(" Total offline phase elapsed time: {:.2e} s\n",
     363            0 :              Timer::Duration(Timer::Now() - t0).count());  // Timing on root
     364              : 
     365              :   // XX TODO: Add output of eigenvalue estimates from the PROM system (and nonlinear EVP
     366              :   // in the general case with wave ports, etc.?)
     367              : 
     368              :   // Main fast frequency sweep loop (online phase).
     369            0 :   Mpi::Print("\nBeginning fast frequency sweep online phase\n");
     370              :   space_op.GetWavePortOp().SetSuppressOutput(false);  // Disable output suppression
     371            0 :   for (const auto &[excitation_idx, excitation_spec] : port_excitations)
     372              :   {
     373            0 :     if (port_excitations.Size() > 1)
     374              :     {
     375            0 :       Mpi::Print("\nSweeping excitation index {:d} ({:d}/{:d}):\n", excitation_idx,
     376            0 :                  excitation_counter, port_excitations.Size());
     377              :     }
     378              :     // Switch paraview subfolders: one for each excitation, if nr_excitations > 1.
     379            0 :     post_op.InitializeParaviewDataCollection(excitation_idx);
     380              : 
     381              :     // Frequency loop.
     382            0 :     for (std::size_t omega_i = 0; omega_i < omega_sample.size(); omega_i++)
     383              :     {
     384            0 :       auto omega = omega_sample[omega_i];
     385            0 :       Mpi::Print("\nIt {:d}/{:d}: ω/2π = {:.3e} GHz (total elapsed time = {:.2e} s)\n",
     386            0 :                  omega_i + 1, omega_sample.size(),
     387            0 :                  iodata.units.Dimensionalize<Units::ValueType::FREQUENCY>(omega),
     388            0 :                  Timer::Duration(Timer::Now() - t0).count());
     389              : 
     390              :       // Assemble and solve the PROM linear system.
     391            0 :       prom_op.SolvePROM(excitation_idx, omega, E);
     392            0 :       Mpi::Print("\n");
     393              : 
     394              :       // Start Post-processing.
     395            0 :       BlockTimer bt0(Timer::POSTPRO);
     396            0 :       Mpi::Print(" Sol. ||E|| = {:.6e}\n", linalg::Norml2(space_op.GetComm(), E));
     397              : 
     398              :       // Compute B = -1/(iω) ∇ x E on the true dofs.
     399            0 :       Curl.Mult(E.Real(), B.Real());
     400            0 :       Curl.Mult(E.Imag(), B.Imag());
     401            0 :       B *= -1.0 / (1i * omega);
     402            0 :       if (space_op.GetMaterialOp().HasWaveVector())
     403              :       {
     404              :         // Calculate B field correction for Floquet BCs.
     405              :         // B = -1/(iω) ∇ x E + 1/ω kp x E
     406            0 :         floquet_corr->AddMult(E, B, 1.0 / omega);
     407              :       }
     408            0 :       post_op.MeasureAndPrintAll(excitation_idx, int(omega_i), E, B, omega);
     409            0 :     }
     410              : 
     411              :     // Final postprocessing & printing: no change to indicator since these are in PROM.
     412            0 :     BlockTimer bt0(Timer::POSTPRO);
     413            0 :     SaveMetadata(prom_op.GetLinearSolver());
     414            0 :   }
     415            0 :   post_op.MeasureFinalize(indicator);
     416            0 :   return indicator;
     417            0 : }
     418              : 
     419              : }  // namespace palace
        

Generated by: LCOV version 2.0-1