From 272200384846fbf71e0e3eb5a06cf3cd62d1b732 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 15:17:55 -0400 Subject: [PATCH 01/57] Add rosenbrock --- GridKit/Solver/Dynamic/CMakeLists.txt | 16 +- GridKit/Solver/Dynamic/Rosenbrock.cpp | 5 + GridKit/Solver/Dynamic/Rosenbrock.hpp | 925 ++++++++++++++++++++++++++ 3 files changed, 945 insertions(+), 1 deletion(-) create mode 100644 GridKit/Solver/Dynamic/Rosenbrock.cpp create mode 100644 GridKit/Solver/Dynamic/Rosenbrock.hpp diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index 2d7fa0152..4d08eb1d0 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -5,7 +5,7 @@ # - Cameron Rutherford #]] -set(_install_headers DynamicSolver.hpp Ida.hpp) +set(_install_headers DynamicSolver.hpp Ida.hpp Rosenbrock.hpp) if(GRIDKIT_ENABLE_SUNDIALS_SPARSE) gridkit_add_library( @@ -42,3 +42,17 @@ else() PUBLIC GridKit::sparse_matrix) endif() + +if (GRIDKIT_ENABLE_RESOLVE) + gridkit_add_library(rosenbrock + SOURCES + Rosenbrock.cpp + HEADERS + ${_install_headers} + LINK_LIBRARIES + PUBLIC ReSolve::ReSolve + PUBLIC GridKit::definitions + PUBLIC GridKit::utilities_logger + PUBLIC GridKit::sparse_matrix + ) +endif() \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp new file mode 100644 index 000000000..c1b16d57e --- /dev/null +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -0,0 +1,5 @@ +#include "Rosenbrock.hpp" + +namespace Integrator { + template class Rosenbrock; +} \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp new file mode 100644 index 000000000..2fb130d5a --- /dev/null +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -0,0 +1,925 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "GridKit/Model/Evaluator.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace Integrator +{ + using State = ReSolve::vector::Vector; + + struct StepControl + { + bool accept; + double step_size; + }; + + class StepController + { + public: + virtual constexpr StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) = 0; + virtual constexpr bool usesError() const = 0; + }; + + class ErrorNorm + { + public: + virtual double errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const = 0; + }; + + template + class Rosenbrock + { + using RealT = typename GridKit::ScalarTraits::RealT; + + public: + struct StepInfo + { + double sim_time; + double step_size; + double next_step_size; + double err_est; + size_t step_no; + bool skip_lu; + bool skip_f; + bool accepted; + + std::string csv_report() const + { + std::stringstream out; + out << std::scientific << std::setprecision(20) + << sim_time << ',' + << step_size << ',' + << next_step_size << ',' + << err_est << ',' + << step_no << ',' + << skip_lu << ',' + << skip_f << ',' + << accepted; + + return out.str(); + } + + std::string report() const + { + std::stringstream out; + out << std::scientific << std::setprecision(20) + << "Simulation Time: " << sim_time << '\n' + << "Step Size: " << step_size << '\n' + << "Next Step Size: " << next_step_size << '\n' + << "Error Estimate: " << err_est << '\n' + << "Step Number: " << step_no << '\n' + << "Skip LU: " << skip_lu << '\n' + << "Skip F: " << skip_f << '\n' + << "Accepted: " << accepted; + + return out.str(); + } + }; + + struct Stats + { + std::vector rejections; + std::vector skip_lu_steps; + size_t num_steps = 0; + size_t f_evals = 0; + size_t f_skipped = 0; + size_t jac_evals = 0; + size_t decomp_solves = 0; + double min_step = INFINITY; + double max_step = 0; + + std::string report() const + { + std::stringstream out; + out << "Rejections: " << rejections.size() + << "\nSteps: " << num_steps + << "\nSkip LU Steps: " << skip_lu_steps.size() + << "\nMin Step: " << min_step + << "\nMax Step: " << max_step + << "\nRHS Function Evals: " << f_evals + << "\nRHS Function Skipped: " << f_skipped + << "\nJacobian Evals: " << jac_evals + << "\nLinear Solves: " << decomp_solves; + + return out.str(); + } + + Stats& operator+=(const Stats& other) + { + rejections.insert(rejections.end(), other.rejections.begin(), other.rejections.end()); + skip_lu_steps.insert(skip_lu_steps.end(), other.skip_lu_steps.begin(), other.skip_lu_steps.end()); + + num_steps += other.num_steps; + f_evals += other.f_evals; + f_skipped += other.f_skipped; + jac_evals += other.jac_evals; + decomp_solves += other.decomp_solves; + + min_step = std::min(min_step, other.min_step); + max_step = std::max(max_step, other.max_step); + + return *this; + } + }; + + struct Parameters + { + double atol = 1e-5; + double rtol = 1e-5; + double starting_step = 1e-5; + size_t max_steps = 2000; + bool skip_lu = false; + }; + + struct Tableau + { + + size_t num_stages; + + /// @brief The coefficient along the diagonal of the Gamma matrix. + RealT gamma; + + /// @brief A vector of sums of rows of the alpha matrix. These are the classic + /// Runge-Kutta 'c' coefficients, or abscissae. + std::unique_ptr alpha_sum; + /// @brief A vector of sums of rows of the Gamma matrix. + std::unique_ptr gamma_sum; + /// @brief A vector of weights for constructing the final solution from the stages + std::unique_ptr m; + + /// @brief Coefficients for the embedded method. If `HAS_EMBEDDED == false`, then + /// this is nothing. + std::unique_ptr e; + + /// @brief The transformed A coefficient matrix. Strictly lower triangular. + std::unique_ptr A; + /// @brief The transformed C coefficient matrix. Strictly lower triangular. + std::unique_ptr C; + + /// @brief The matrix of dense coefficients. + std::unique_ptr H; + + /// @brief What ODE order these coefficients satisfy. + uint8_t order; + + /// @brief Whether or not these coefficients satisfy Rosenbrock-Krylov (ROK) order conditions up to + /// `order`. + bool is_krylov; + /// @brief Whether or not these coefficients satisfy Rosenbrock-W (ROW) order conditions up to `order`. + bool is_w; + /// @brief Whether or not these coefficients satisfy DAE order conditions up to `order`. + bool is_dae; + + /// @brief Whether or not this tableau contains an embedded method + constexpr bool has_embedded() const + { + return e == nullptr; + } + + constexpr bool hasDenseOutput() const + { + return static_cast(H); + } + + constexpr RealT getA(size_t row, size_t col) const + { + return A[row * num_stages + col]; + } + + constexpr bool can_reuse_asum(size_t stage) const + { + assert(stage < num_stages); + + if (stage == 0) + return false; + else + { + for (size_t j = 0; j < stage - 1; j++) + { + if (getA(stage, j) != getA(stage - 1, j)) + { + return false; + } + } + return true; + } + } + + constexpr bool can_reuse_asum_for_out() const + { + if (num_stages == 1) + return false; + + for (size_t j = 0; j < num_stages - 1; j++) + { + if (getA(num_stages - 1, j) != m[j]) + { + return false; + } + } + + return true; + } + + constexpr std::tuple error_estimator_stage() const + { + std::tuple re = {false, 0}; + for (size_t j = 0; j < num_stages; j++) + { + if (e[j] == 1.0 && !std::get<0>(re)) + { + re = {true, j}; + } + else if (e[j] != 0.0) + { + return {false, 0}; + } + } + return re; + } + + static constexpr Tableau lin_implicit_euler() + { + constexpr size_t num_stages = 1; + + Tableau re = { + .num_stages = 1, + .gamma = 1.0, + .alpha_sum = std::make_unique_for_overwrite(num_stages), + .gamma_sum = std::make_unique_for_overwrite(num_stages), + .m = std::make_unique_for_overwrite(num_stages), + .e = {}, + .A = std::make_unique_for_overwrite(num_stages * num_stages), + .C = std::make_unique_for_overwrite(num_stages * num_stages), + .H = {}, + .order = 1, + .is_krylov = true, + .is_w = false, + .is_dae = true, + }; + + re.alpha_sum[0] = 0.0; + + re.gamma_sum[0] = 1.0; + + re.m[0] = 1.0; + + re.A[0] = 0.0; + + re.C[0] = 2.0; + + return std::move(re); + } + }; + + private: + double step_size_ = 0; + double prev_step_size_ = 0; + + bool skip_lu_ = false; + bool skip_f_ = false; + + /// @brief Keeps track of whether or not the integrator currently has valid dense coefficients. + /// i.e. they have been computed and haven't been invalidated by taking another step. + bool dense_coefficients_valid = false; + + Tableau tab_; + + GridKit::Model::Evaluator* model_; + ReSolve::SystemSolver& lin_solver_; + ReSolve::VectorHandler& vector_handler_; + const ErrorNorm* err_norm_; + ReSolve::memory::MemorySpace memspace_; + + double current_time_; + std::unique_ptr y_prev_; + std::unique_ptr y_cur_; + std::unique_ptr y_interp_; + + Parameters params_; + Stats stats_; + + Rosenbrock(const Rosenbrock& other) = delete; + Rosenbrock(Rosenbrock&& other) = delete; + + public: + Rosenbrock(Tableau&& tab, + GridKit::Model::Evaluator* model, + ReSolve::SystemSolver& lin_solver, + ReSolve::VectorHandler& vector_handler, + const ErrorNorm* err_norm, + ReSolve::memory::MemorySpace memspace = ReSolve::memory::HOST) + : tab_(std::move(tab)), + model_(model), + lin_solver_(lin_solver), + vector_handler_(vector_handler), + err_norm_(err_norm), + memspace_(memspace) + { + } + + int allocate() + { + size_t size = static_cast(model_->size()); + + y_prev_ = std::make_unique(size); + y_cur_ = std::make_unique(size); + y_interp_ = std::make_unique(size); + asum_ = std::make_unique(size); + csum_ = std::make_unique(size); + RHS_ = std::make_unique(size); + RHS_first_stage_ = std::make_unique(size); + dFdt_ = std::make_unique(size); + mass_ = std::make_unique(size); + y_new_ = std::make_unique(size); + + y_prev_->allocate(memspace_); + y_cur_->allocate(memspace_); + y_interp_->allocate(memspace_); + asum_->allocate(memspace_); + csum_->allocate(memspace_); + RHS_->allocate(memspace_); + RHS_first_stage_->allocate(memspace_); + dFdt_->allocate(memspace_); + mass_->allocate(memspace_); + y_new_->allocate(memspace_); + + if (tab_.e) + { + auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); + if (!can_use_stage) + { + err_est_ = std::make_unique(size); + err_est_->allocate(memspace_); + } + } + + stages_ = std::make_unique[]>(tab_.num_stages); + for (size_t i = 0; i < tab_.num_stages; i++) + { + stages_[i] = std::make_unique(size); + stages_[i]->allocate(memspace_); + } + + if (tab_.order > 2) + { + dense_coeff_ = std::make_unique[]>(tab_.order - 2); + for (size_t i = 0; i < tab_.order - 2; i++) + { + dense_coeff_[i] = std::make_unique(size); + dense_coeff_[i]->allocate(memspace_); + } + } + + jacobian_ = std::make_unique(size, size, model_->getCsrJacobian()->getNnz()); + + return 0; + } + + int initializeSimulation(RealT t0) + { + current_time_ = t0; + y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_); + jacobian_analyzed_ = false; + + GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); + jacobian_->setDataPointers( + model_jacobian->getRowData(), + model_jacobian->getColData(), + model_jacobian->getValues(), + memspace_); + lin_solver_.setMatrix(jacobian_.get()); + lin_solver_.analyze(); + lin_solver_.preconditionerSetup(); + + assert(model_->tag().size() == static_cast(model_->size())); + std::unique_ptr mass = std::make_unique(model_->tag().size()); + for (size_t i = 0; i < static_cast(model_->size()); i++) + { + mass[i] = model_->tag()[i] ? 1.0 : 0.0; + } + mass_->copyFromExternal(mass.get(), memspace_, memspace_); + + return 0; + } + + int integrate(const std::vector& out_times, + StepController& step_controller, + Parameters params = {}, + std::optional> out_cb = {}, + std::optional> step_cb = {}) + { + skip_lu_ = false; + skip_f_ = false; + + bool prev_accept = true; + step_size_ = params.starting_step; + + double next_step_size; + + // Kahan summation time buffer. The "leftover" time that was lost when trying to add h at some point that needs to be added + // later + double time_buffer = 0; + + for (double out_time : out_times) + { + while (current_time_ < out_time && stats_.num_steps < params.max_steps) + { + // TODO: This assumes the step cannot fail. + time_step(current_time_, step_size_); + + double err = 0; + + if (step_controller.usesError()) + { + if (err_norm_ == nullptr) + { + std::cerr << "The provided step controller requires the use of an error norm, but none was provided!\n"; + + return -1; + } + + State& err_vec = error_estimate(); + err = err_norm_->errorNorm(err_vec, *y_new_, *y_cur_, vector_handler_, memspace_); + } + + StepControl next_step = step_controller.nextStep(err, + StepControl{ + .accept = prev_accept, + .step_size = step_size_, + }, + tab_.order); + prev_accept = next_step.accept; + next_step_size = next_step.step_size; + + if (prev_accept) + { + // Try to add the leftover time that we've stored up + double step_size_adj = step_size_ + time_buffer; + double next_time = current_time_ + step_size_adj; + + // Kahan summation - keep track of how much of step_size_adj we weren't able to add to current_time + // due to lack of precision + time_buffer = step_size_adj - (next_time - current_time_); + current_time_ = next_time; + + // step_cb(current_time, yout); + skip_f_ = false; + + stats_.num_steps++; + if (skip_lu_) + { + stats_.skip_lu_steps.push_back(StepInfo{ + .sim_time = current_time_, + .step_size = step_size_, + .next_step_size = next_step_size, + .err_est = err, + .step_no = stats_.num_steps, + .skip_lu = skip_lu_, + .skip_f = skip_f_, + .accepted = prev_accept, + }); + } + stats_.min_step = std::min(stats_.min_step, step_size_); + stats_.max_step = std::max(stats_.max_step, step_size_); + + std::swap(y_prev_, y_cur_); + std::swap(y_cur_, y_new_); + dense_coefficients_valid = false; + } + else + { + skip_f_ = true; + + stats_.rejections.push_back(StepInfo{ + .sim_time = current_time_, + .step_size = step_size_, + .next_step_size = next_step_size, + .err_est = err, + .step_no = stats_.num_steps, + .skip_lu = skip_lu_, + .skip_f = skip_f_, + .accepted = prev_accept, + }); + } + + double step_gain = next_step_size / step_size_; + if (params.skip_lu && step_gain >= 1 && step_gain <= 1.2) + { + skip_lu_ = true; + } + else + { + skip_lu_ = false; + prev_step_size_ = step_size_; + step_size_ = next_step_size; + } + + if (step_cb) + { + y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_); + model_->updateTime(current_time_, 0.0); + + (*step_cb)(StepInfo{ + .sim_time = current_time_, + .step_size = step_size_, + .next_step_size = next_step_size, + .err_est = err, + .step_no = stats_.num_steps, + .skip_lu = skip_lu_, + .skip_f = skip_f_, + .accepted = prev_accept, + }); + } + } + + if (current_time_ >= out_time) + { + if (tab_.hasDenseOutput()) + { + if (!dense_coefficients_valid) + { + calc_dense_coeff(); + dense_coefficients_valid = true; + } + + double theta = (out_time - current_time_) / prev_step_size_ + 1; + interp_dense(theta); + } + else + { + // TODO: Put code for alternative interpolation (Abdou) here + double theta = (out_time - current_time_) / prev_step_size_ + 1; + y_interp_->copyFromExternal(y_prev_.get(), memspace_, memspace_); + vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); + vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); + } + + if (out_cb) + { + y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_); + model_->updateTime(out_time, 0.0); + + (*out_cb)(out_time); + } + } + else + { + y_interp_->copyFromExternal(y_cur_.get(), memspace_, memspace_); + break; + } + } + + return 0; + } + + private: + std::unique_ptr asum_; + std::unique_ptr csum_; + std::unique_ptr RHS_; + std::unique_ptr RHS_first_stage_; + std::unique_ptr dFdt_; + std::unique_ptr mass_; + std::unique_ptr y_new_; + std::unique_ptr err_est_; + + std::unique_ptr jacobian_; + bool jacobian_analyzed_ = false; + + std::unique_ptr[]> stages_; + std::unique_ptr[]> dense_coeff_; + + public: + int time_step(double t0, double dt) + { + bool y0_copied = false; + + // Form the left-hand side of the system + // Can sometimes be skipped if the method is a w-method + [[likely]] + if (!tab_.is_w || !skip_lu_) + { + double dtXgamma = -1.0 / (dt * tab_.gamma); + y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_); + y0_copied = true; + model_->updateTime(t0, -dtXgamma); + model_->evaluateJacobian(); + GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); + jacobian_->setDataPointers( + model_jacobian->getRowData(), + model_jacobian->getColData(), + model_jacobian->getValues(), + memspace_); + + std::cerr << "Jacobian values:\n"; + for (size_t i = 0; i < model_jacobian->getNnz(); i++) + { + std::cerr << std::scientific << std::setprecision(5) << std::setw(12) << model_jacobian->getValues()[i] << ' '; + } + std::cerr << "\n\n"; + + [[likely]] + if (jacobian_analyzed_) + { + lin_solver_.refactorize(); + } + else + { + lin_solver_.factorize(); + } + + stats_.jac_evals++; + } + + // First stage + [[unlikely]] + if (skip_f_) + { + stats_.f_skipped++; + } + else + { + // TODO: non-autonomous model + if (!y0_copied) + { + y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_); + y0_copied = true; + } + model_->updateTime(t0, 0.0); + model_->evaluateResidual(); + RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); + vector_handler_.scal(-1, RHS_first_stage_.get(), memspace_); + + std::cerr << "RHS_first_stage_:\n"; + for (size_t i = 0; i < RHS_first_stage_->getSize(); i++) + { + std::cerr << std::scientific << std::setprecision(5) << std::setw(12) << RHS_first_stage_->getData(memspace_)[i] << ' '; + } + std::cerr << "\n\n"; + + stats_.f_evals++; + } + lin_solver_.solve(RHS_first_stage_.get(), stages_[0].get()); + stats_.decomp_solves++; + + std::cerr << "Stage 0:\n"; + for (size_t i = 0; i < stages_[0]->getSize(); i++) + { + std::cerr << std::scientific << std::setprecision(5) << std::setw(12) << stages_[0]->getData(memspace_)[i] << ' '; + } + std::cerr << "\n\n"; + + // Rest of stages + for (size_t i = 1; i < tab_.num_stages; i++) + { + // Calculate asum + // We can sometimes reuse asum from the previous stage + if (i > 1 && tab_.can_reuse_asum(i)) + { + if (tab_.A[tab_.num_stages * i + i - 1] != 0.0) + vector_handler_.axpy(tab_.A[tab_.num_stages * i + i - 1], stages_[i - 1].get(), asum_.get(), memspace_); + } + else + { + asum_->copyFromExternal(y_cur_.get(), memspace_, memspace_); + + for (size_t j = 0; j < i; j++) + { + if (tab_.A[tab_.num_stages * i + j] != 0.0) + vector_handler_.axpy(tab_.A[tab_.num_stages * i + j], stages_[j].get(), asum_.get(), memspace_); + } + } + + // Calculate csum + // TODO: Since csum is multiplied by the mass matrix, we can reduce calculations by just not calculating some indices + csum_->setToZero(memspace_); + for (size_t j = 0; j < i; j++) + { + if (tab_.C[i * tab_.num_stages + j] != 0.0) + { + vector_handler_.axpy(tab_.C[i * tab_.num_stages + j] / dt, stages_[j].get(), csum_.get(), memspace_); + } + } + + // TODO: non-autonomous model + asum_->copyToExternal(model_->y().data(), memspace_, memspace_); + model_->updateTime(t0 + tab_.alpha_sum[i] * dt, 0.0); + model_->evaluateResidual(); + RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); + + vector_handler_.scal(-1, RHS_.get(), memspace_); + vector_handler_.scal(mass_.get(), csum_.get(), memspace_); + vector_handler_.axpy(-1, csum_.get(), RHS_.get(), memspace_); + + lin_solver_.solve(RHS_.get(), stages_[i].get()); + stats_.f_evals++; + stats_.decomp_solves++; + } + + // Compute the solution at time t + dt + // It happens often where the solution is just asum of the last stage + // plus some multiple of the last stage. In that case we can avoid a matmul + if (tab_.can_reuse_asum_for_out()) + { + std::swap(asum_, y_new_); + vector_handler_.axpy(tab_.m[tab_.num_stages - 1], stages_[tab_.num_stages - 1].get(), y_new_.get(), memspace_); + } + else + { + y_new_->copyFromExternal(y_cur_.get(), memspace_, memspace_); + + for (size_t j = 0; j < tab_.num_stages; j++) + { + if (tab_.m[j] != 0.0) + { + vector_handler_.axpy(tab_.m[j], stages_[j].get(), y_new_.get(), memspace_); + } + } + } + + return 0; + } + + State& error_estimate() const + { + auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); + + if (can_use_stage) + { + return *stages_[err_stage]; + } + else + { + err_est_->copyFromExternal(stages_[0].get(), memspace_, memspace_); + vector_handler_.scal(tab_.e[0], stages_[0].get(), memspace_); + for (size_t j = 1; j < tab_.num_stages; j++) + { + if (tab_.e[j] != 0.0) + { + vector_handler_.axpy(tab_.e[j], stages_[j].get(), err_est_.get(), memspace_); + } + } + + return *err_est_; + } + } + + constexpr bool has_dense_output() + { + return static_cast(tab_.H); + } + + void calc_dense_coeff() + { + if (tab_.order > 2) + { + for (size_t j = 0; j < tab_.order - 2; j++) + { + dense_coeff_[j]->setToZero(memspace_); + } + + for (size_t i = 0; i < tab_.num_stages; i++) + { + for (size_t j = 0; j < tab_.order - 2; j++) + { + vector_handler_.axpy(tab_.H[j * (tab_.num_stages - 2) + i], stages_[i].get(), dense_coeff_[j].get(), memspace_); + } + } + } + } + + // TODO: Maybe this can be integrated into OneStepIntegrator? + int interp_dense(double theta) + { + double one = 1.0; + if (tab_.order > 2) + { + y_interp_->copyFromExternal(dense_coeff_[tab_.order - 3].get(), memspace_, memspace_); + + for (size_t i = 1; i < tab_.order - 2; i++) + { + vector_handler_.scal(theta, y_interp_.get(), memspace_); + vector_handler_.axpy(one, dense_coeff_[tab_.order - 3 - i].get(), y_interp_.get(), memspace_); + } + } + else + { + y_interp_->setToZero(memspace_); + } + + double omt = 1 - theta; + + vector_handler_.scal(omt, y_interp_.get(), memspace_); + vector_handler_.axpy(one, y_cur_.get(), y_interp_.get(), memspace_); + vector_handler_.scal(theta, y_interp_.get(), memspace_); + vector_handler_.axpy(omt, y_prev_.get(), y_interp_.get(), memspace_); + + return 0; + } + }; + + class AdaptiveStep : public StepController + { + struct Parameters + { + double facmin = 0.2; + double facmax = 5.0; + double facscale = 0.9; + } params_; + + public: + AdaptiveStep(const Parameters& params) : params_(params) + { + } + + constexpr StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) final + { + StepControl next_step = prev_step; + + double h_mult = std::min(params_.facmax, std::max(params_.facscale * std::pow(err, -1.0 / method_order), params_.facmin)); + + next_step.accept = err <= 1; + next_step.step_size *= h_mult; + + return next_step; + } + + constexpr bool usesError() const final + { + return true; + } + }; + + class FixedStep : public StepController + { + StepControl nextStep([[maybe_unused]] double err, [[maybe_unused]] StepControl prev_step, [[maybe_unused]] uint8_t method_order) + { + return StepControl{ + .accept = true, + .step_size = prev_step.step_size, + }; + } + + bool usesError() const final + { + return false; + } + }; + + class InfNorm : public ErrorNorm + { + mutable struct + { + std::unique_ptr out_; + std::unique_ptr scale_; + std::unique_ptr yprev_abs_; + } workspace_; + + public: + struct Parameters + { + std::unique_ptr atol; + double rtol; + } params_; + + InfNorm(Parameters&& params) : params_(std::move(params)) + { + } + + double errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const final + { + double one = 1.0; + workspace_.out_->copyFromExternal(&err, memspace, memspace); + workspace_.scale_->copyFromExternal(&y, memspace, memspace); + workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace); + + handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); + handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); + handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.yprev_abs_.get(), memspace); + handler.scal(params_.rtol, workspace_.scale_.get(), memspace); + handler.axpy(one, params_.atol.get(), workspace_.scale_.get(), memspace); + handler.scaleInv(workspace_.scale_.get(), workspace_.out_.get(), memspace); + + return handler.amax(workspace_.out_.get(), memspace); + } + }; +} // namespace Integrator \ No newline at end of file From bd2c5f93320089c92f7999b3a74e7a6179184ff1 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 15:18:02 -0400 Subject: [PATCH 02/57] Add rosenbrock tests --- tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 5 + .../Solver/Dynamic/RosenbrockTests.hpp | 425 ++++++++++++++++++ .../Solver/Dynamic/runRosenbrockTests.cpp | 14 + 3 files changed, 444 insertions(+) create mode 100644 tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp create mode 100644 tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index 93b92a471..4029f83bb 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -6,4 +6,9 @@ target_link_libraries( add_test(NAME IDATest COMMAND $) +add_executable(test_ros runRosenbrockTests.cpp) +target_link_libraries(test_ros GridKit::rosenbrock GridKit::testing) +add_test(NAME ROSTest COMMAND $) + + install(TARGETS test_ida RUNTIME DESTINATION bin) diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp new file mode 100644 index 000000000..ff3827523 --- /dev/null +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -0,0 +1,425 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include "GridKit/LinearAlgebra/SparseMatrix/CsrMatrix.hpp" +#include +#include + +namespace GridKit +{ + namespace Model + { + template + class TrigonometricDaeEvaluator : public Model::Evaluator + { + public: + using RealT = typename Model::Evaluator::RealT; + + constexpr static size_t SIZE = 2; + + TrigonometricDaeEvaluator() + { + } + + int allocate() override + { + constexpr size_t NNZ = SIZE * SIZE; + + IdxT* row_ptrs = new IdxT[SIZE + 1]; + IdxT* cols = new IdxT[NNZ]; + RealT* vals = new RealT[NNZ]; + + for (size_t i = 0; i < SIZE + 1; i++) + { + row_ptrs[i] = static_cast(i * SIZE); + } + + for (size_t i = 0; i < SIZE; i++) + { + for (size_t j = 0; j < SIZE; j++) + { + cols[i * SIZE + j] = static_cast(j); + } + } + + csr_jac_ = std::make_unique>(SIZE, SIZE, NNZ, &row_ptrs, &cols, &vals); + + return 0; + } + + int initialize() override + { + y_ = {sinh(0.5), tanh(0.5)}; + yp_ = {0.0, 0.0}; + + tag_ = {true, false}; + + f_ = {0.0, 0.0}; + g_ = {0.0, 0.0}; + return 0; + } + + IdxT size() override + { + return SIZE; + } + + IdxT nnz() override + { + return SIZE * SIZE; + } + + bool hasJacobian() override + { + return true; + } + + IdxT sizeQuadrature() override + { + return 0; + } + + IdxT sizeParams() override + { + return 0; + } + + void setTolerances([[maybe_unused]] RealT& rel_tol, [[maybe_unused]] RealT& abs_tol) const override + { + } + + void setMaxSteps(IdxT& msa) const override + { + msa = 2000; + } + + int tagDifferentiable() override + { + return 0; + } + + int evaluateResidual() override + { + double y02 = y_[0] * y_[0]; + double y12 = y_[1] * y_[1]; + + f_ = {y02 / (y_[1] * sqrt(std::pow(y_[0] / y_[1], 2) - 1)), // + y12 + 1 / (1 + y02) - (y02 / y12 - y02)}; + + return 0; + } + + int evaluateJacobian() override + { + RealT* vals = csr_jac_->getValues(); + + double y1 = y_[0]; + double y2 = y_[1]; + + double y12 = y1 * y1; + double y13 = y12 * y1; + double y22 = y2 * y2; + double y23 = y22 * y2; + double y24 = y22 * y22; + + double tmp = std::pow(y12 / y22 - 1, 1.5); + double tmp2 = std::pow(y12 + 1, 2); + + vals[0] = alpha_ + -(-y13 + 2 * y1 * y22) / (y23 * tmp); + vals[1] = y12 / (y22 * tmp); + vals[2] = -2 * y1 * (1 / y22 - 1) - (2 * y1) / tmp2; + vals[3] = (2 * (y12 + y24)) / y23; + + return 0; + } + + int evaluateIntegrand() override + { + return 0; + } + + int initializeAdjoint() override + { + return 0; + } + + int evaluateAdjointResidual() override + { + return 0; + } + + int evaluateAdjointIntegrand() override + { + return 0; + } + + void updateTime([[maybe_unused]] RealT t, RealT a) override + { + alpha_ = a; + } + + std::vector& y() override + { + return y_; + } + + const std::vector& y() const override + { + return y_; + } + + std::vector& yp() override + { + return yp_; + } + + const std::vector& yp() const override + { + return yp_; + } + + std::vector& tag() override + { + return tag_; + } + + const std::vector& tag() const override + { + return tag_; + } + + std::vector& yB() override + { + return yB_; + } + + const std::vector& yB() const override + { + return yB_; + } + + std::vector& ypB() override + { + return ypB_; + } + + const std::vector& ypB() const override + { + return ypB_; + } + + std::vector& param() override + { + return param_; + } + + const std::vector& param() const override + { + return param_; + } + + std::vector& param_up() override + { + return param_up_; + } + + const std::vector& param_up() const override + { + return param_up_; + } + + std::vector& param_lo() override + { + return param_lo_; + } + + const std::vector& param_lo() const override + { + return param_lo_; + } + + std::vector& getResidual() override + { + return f_; + } + + const std::vector& getResidual() const override + { + return f_; + } + + GridKit::LinearAlgebra::COO_Matrix& getJacobian() override + { + return jac_; + } + + const GridKit::LinearAlgebra::COO_Matrix& getJacobian() const override + { + return jac_; + } + + std::vector& getIntegrand() override + { + return g_; + } + + const std::vector& getIntegrand() const override + { + return g_; + } + + std::vector& getAdjointResidual() override + { + return fB_; + } + + const std::vector& getAdjointResidual() const override + { + return fB_; + } + + std::vector& getAdjointIntegrand() override + { + return gB_; + } + + const std::vector& getAdjointIntegrand() const override + { + return gB_; + } + + IdxT getIDcomponent() + { + return 0; + } + + GridKit::LinearAlgebra::CsrMatrix* getCsrJacobian() const override + { + return csr_jac_.get(); + } + + protected: + std::vector y_; + std::vector yp_; + std::vector tag_; + std::vector f_; + std::vector g_; + + std::vector yB_; + std::vector ypB_; + std::vector fB_; + std::vector gB_; + + GridKit::LinearAlgebra::COO_Matrix jac_; + std::unique_ptr> csr_jac_; + + RealT alpha_; + + std::vector param_; + std::vector param_up_; + std::vector param_lo_; + }; + } // namespace Model + + namespace Testing + { + template + class RosenbrockTests + { + public: + TestOutcome test() + { + using Rosenbrock = Integrator::Rosenbrock; + TestStatus success = true; + + Model::TrigonometricDaeEvaluator model; + model.allocate(); + model.initialize(); + + ReSolve::LinAlgWorkspaceCpu linear_workspace; + // ReSolve::SystemSolver lin_solver(&linear_workspace, "klu", "klu", "klu"); + ReSolve::SystemSolver lin_solver(&linear_workspace, "none", "none", "randgmres", "ilu0"); + ReSolve::VectorHandler vec_handler; + + lin_solver.initialize(); + + Rosenbrock integrator(Rosenbrock::Tableau::lin_implicit_euler(), &model, lin_solver, vec_handler, nullptr); + integrator.allocate(); + + Integrator::FixedStep step_controller; + + double final_time = 2.0; + std::vector out_times = {final_time}; + + double step_exponent_lower = -1.0; + double step_exponent_upper = -5.0; + size_t num_samples = 11; + + std::vector step_sizes; + std::vector errors; + + auto out_cb = [&]([[maybe_unused]] double t) + { + double error = 0.0; + double sol_norm = 0.0; + + const std::vector& state = model.y(); + + error += std::pow(state[0] - sinh(final_time), 2); + error += std::pow(state[1] - tanh(final_time), 2); + + sol_norm += std::pow(sinh(final_time), 2); + sol_norm += std::pow(tanh(final_time), 2); + + errors.push_back(std::sqrt(error) / std::sqrt(sol_norm)); + + std::cerr << "Callback called at t = " << t << '\n'; + }; + + auto step_cb = [&](const Rosenbrock::StepInfo& step_info) + { + std::cerr << step_info.report() << '\n' + << "Current state:\n" + << model.y()[0] << ',' << model.y()[1] << '\n' + << std::sinh(step_info.sim_time) << ',' << std::tanh(step_info.sim_time) << "\n\n"; + }; + + for (size_t i = 0; i < 1; i++) + { + double step_size = std::pow(10, step_exponent_lower + static_cast(i) * (step_exponent_upper - step_exponent_lower) / static_cast(num_samples - 1)); + step_sizes.push_back(step_size); + + model.initialize(); + integrator.initializeSimulation(0.5); + + typename Rosenbrock::Parameters params; + params.starting_step = step_size; + integrator.integrate(out_times, step_controller, params, out_cb, step_cb); + } + + std::cerr << "Step sizes\n"; + for (double step_size : step_sizes) + { + std::cerr << std::scientific << std::setprecision(20) << step_size << ' '; + } + + std::cerr << "\n\nErrors\n"; + for (double error : errors) + { + std::cerr << std::scientific << std::setprecision(20) << error << ' '; + } + std::cerr << '\n'; + + return success.report(__func__); + } + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp new file mode 100644 index 000000000..97fdfe58a --- /dev/null +++ b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp @@ -0,0 +1,14 @@ +#include "RosenbrockTests.hpp" + +int main() +{ + using namespace GridKit; + using namespace GridKit::Testing; + + GridKit::Testing::TestingResults result; + GridKit::Testing::RosenbrockTests test; + + result += test.test(); + + return result.summary(); +} From dd9ebb11c33544923f70ccd68597e9df1592d27b Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 17:00:28 -0400 Subject: [PATCH 03/57] Fixed bug setting alpha coefficient in rosenbrock --- GridKit/Solver/Dynamic/Rosenbrock.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 2fb130d5a..5bcc5a861 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -618,10 +618,9 @@ namespace Integrator [[likely]] if (!tab_.is_w || !skip_lu_) { - double dtXgamma = -1.0 / (dt * tab_.gamma); y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_); y0_copied = true; - model_->updateTime(t0, -dtXgamma); + model_->updateTime(t0, -1.0 / (dt * tab_.gamma)); model_->evaluateJacobian(); GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); jacobian_->setDataPointers( From 9d15c72ed1eb4df70ce497d47c50ffcfab249c82 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 17:00:42 -0400 Subject: [PATCH 04/57] Removed extra move Allowing for the possibility of return values optimization --- GridKit/Solver/Dynamic/Rosenbrock.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 5bcc5a861..7b19e9743 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -287,7 +287,7 @@ namespace Integrator re.C[0] = 2.0; - return std::move(re); + return re; } }; From fb45769e69d797612d582b310128c23bc3fd8e48 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 17:03:42 -0400 Subject: [PATCH 05/57] Remove some debug outputs --- GridKit/Solver/Dynamic/Rosenbrock.hpp | 21 ------------------- .../Solver/Dynamic/RosenbrockTests.hpp | 10 --------- 2 files changed, 31 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 7b19e9743..9e9104867 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -629,13 +629,6 @@ namespace Integrator model_jacobian->getValues(), memspace_); - std::cerr << "Jacobian values:\n"; - for (size_t i = 0; i < model_jacobian->getNnz(); i++) - { - std::cerr << std::scientific << std::setprecision(5) << std::setw(12) << model_jacobian->getValues()[i] << ' '; - } - std::cerr << "\n\n"; - [[likely]] if (jacobian_analyzed_) { @@ -668,25 +661,11 @@ namespace Integrator RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); vector_handler_.scal(-1, RHS_first_stage_.get(), memspace_); - std::cerr << "RHS_first_stage_:\n"; - for (size_t i = 0; i < RHS_first_stage_->getSize(); i++) - { - std::cerr << std::scientific << std::setprecision(5) << std::setw(12) << RHS_first_stage_->getData(memspace_)[i] << ' '; - } - std::cerr << "\n\n"; - stats_.f_evals++; } lin_solver_.solve(RHS_first_stage_.get(), stages_[0].get()); stats_.decomp_solves++; - std::cerr << "Stage 0:\n"; - for (size_t i = 0; i < stages_[0]->getSize(); i++) - { - std::cerr << std::scientific << std::setprecision(5) << std::setw(12) << stages_[0]->getData(memspace_)[i] << ' '; - } - std::cerr << "\n\n"; - // Rest of stages for (size_t i = 1; i < tab_.num_stages; i++) { diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index ff3827523..e76d0b2ab 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -380,16 +380,6 @@ namespace GridKit sol_norm += std::pow(tanh(final_time), 2); errors.push_back(std::sqrt(error) / std::sqrt(sol_norm)); - - std::cerr << "Callback called at t = " << t << '\n'; - }; - - auto step_cb = [&](const Rosenbrock::StepInfo& step_info) - { - std::cerr << step_info.report() << '\n' - << "Current state:\n" - << model.y()[0] << ',' << model.y()[1] << '\n' - << std::sinh(step_info.sim_time) << ',' << std::tanh(step_info.sim_time) << "\n\n"; }; for (size_t i = 0; i < 1; i++) From 7c58a56dd9eb40f5994e017e660399d9b4fa1d0c Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 17:03:57 -0400 Subject: [PATCH 06/57] Properly reset stats in initializeSimulation --- GridKit/Solver/Dynamic/Rosenbrock.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 9e9104867..627177732 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -419,6 +419,8 @@ namespace Integrator } mass_->copyFromExternal(mass.get(), memspace_, memspace_); + stats_ = Stats(); + return 0; } From 22829e141fb60ad3979f1e3954d6018bf9d20679 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 17:04:18 -0400 Subject: [PATCH 07/57] Switch back to klu solver --- tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index e76d0b2ab..dd9b48423 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -345,8 +345,7 @@ namespace GridKit model.initialize(); ReSolve::LinAlgWorkspaceCpu linear_workspace; - // ReSolve::SystemSolver lin_solver(&linear_workspace, "klu", "klu", "klu"); - ReSolve::SystemSolver lin_solver(&linear_workspace, "none", "none", "randgmres", "ilu0"); + ReSolve::SystemSolver lin_solver(&linear_workspace, "klu", "klu", "klu"); ReSolve::VectorHandler vec_handler; lin_solver.initialize(); From e719466dff7ea3a9d61f9a25db6a15637ab85c6a Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 17:05:17 -0400 Subject: [PATCH 08/57] Add empirical order testing Also properly configure the max_steps parameter --- .../Solver/Dynamic/RosenbrockTests.hpp | 28 +++++++++++++++---- .../Solver/Dynamic/runRosenbrockTests.cpp | 3 +- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index dd9b48423..39e9ea715 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -334,12 +334,15 @@ namespace GridKit template class RosenbrockTests { + using Rosenbrock = Integrator::Rosenbrock; + public: - TestOutcome test() + TestOutcome test_order(Rosenbrock::Tableau&& tab) { - using Rosenbrock = Integrator::Rosenbrock; TestStatus success = true; + uint8_t expected_order = tab.order; + Model::TrigonometricDaeEvaluator model; model.allocate(); model.initialize(); @@ -350,7 +353,7 @@ namespace GridKit lin_solver.initialize(); - Rosenbrock integrator(Rosenbrock::Tableau::lin_implicit_euler(), &model, lin_solver, vec_handler, nullptr); + Rosenbrock integrator(std::move(tab), &model, lin_solver, vec_handler, nullptr); integrator.allocate(); Integrator::FixedStep step_controller; @@ -381,7 +384,7 @@ namespace GridKit errors.push_back(std::sqrt(error) / std::sqrt(sol_norm)); }; - for (size_t i = 0; i < 1; i++) + for (size_t i = 0; i < num_samples; i++) { double step_size = std::pow(10, step_exponent_lower + static_cast(i) * (step_exponent_upper - step_exponent_lower) / static_cast(num_samples - 1)); step_sizes.push_back(step_size); @@ -391,7 +394,8 @@ namespace GridKit typename Rosenbrock::Parameters params; params.starting_step = step_size; - integrator.integrate(out_times, step_controller, params, out_cb, step_cb); + params.max_steps = static_cast(ceil((final_time - 0.5) / step_size)) + 10; + integrator.integrate(out_times, step_controller, params, out_cb); } std::cerr << "Step sizes\n"; @@ -405,7 +409,19 @@ namespace GridKit { std::cerr << std::scientific << std::setprecision(20) << error << ' '; } - std::cerr << '\n'; + std::cerr << "\n\n"; + + std::vector pairwise_orders; + double empirical_order = 0.0; + for (size_t i = 1; i < num_samples; i++) + { + pairwise_orders.push_back((log(errors[i]) - log(errors[i - 1])) / (log(step_sizes[i]) - log(step_sizes[i - 1]))); + empirical_order += pairwise_orders.back(); + } + empirical_order /= static_cast(num_samples - 1); + + std::cerr << "Empirical order: " << std::fixed << std::setprecision(5) << empirical_order << " v.s. expected " << static_cast(expected_order) << '\n'; + success *= empirical_order > expected_order * 0.85; return success.report(__func__); } diff --git a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp index 97fdfe58a..a7db3cceb 100644 --- a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp +++ b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp @@ -1,3 +1,4 @@ +#include "GridKit/Solver/Dynamic/Rosenbrock.hpp" #include "RosenbrockTests.hpp" int main() @@ -8,7 +9,7 @@ int main() GridKit::Testing::TestingResults result; GridKit::Testing::RosenbrockTests test; - result += test.test(); + result += test.test_order(Integrator::Rosenbrock::Tableau::lin_implicit_euler()); return result.summary(); } From bfc88c772107fce5cad43e05db3d65229847b0fc Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 18:30:13 -0400 Subject: [PATCH 09/57] Fix dense output indexing issue --- GridKit/Solver/Dynamic/Rosenbrock.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 627177732..ca025b0b7 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -781,7 +781,7 @@ namespace Integrator { for (size_t j = 0; j < tab_.order - 2; j++) { - vector_handler_.axpy(tab_.H[j * (tab_.num_stages - 2) + i], stages_[i].get(), dense_coeff_[j].get(), memspace_); + vector_handler_.axpy(tab_.H[j * tab_.num_stages + i], stages_[i].get(), dense_coeff_[j].get(), memspace_); } } } From b69607362e2cb4aeb48feffaa1a5baa28d474832 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 18:30:34 -0400 Subject: [PATCH 10/57] Add RODAS5P tableau --- GridKit/Solver/Dynamic/Rosenbrock.hpp | 149 +++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index ca025b0b7..b881ca213 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -262,7 +262,7 @@ namespace Integrator constexpr size_t num_stages = 1; Tableau re = { - .num_stages = 1, + .num_stages = num_stages, .gamma = 1.0, .alpha_sum = std::make_unique_for_overwrite(num_stages), .gamma_sum = std::make_unique_for_overwrite(num_stages), @@ -289,6 +289,153 @@ namespace Integrator return re; } + + static constexpr Tableau rodas5p() + { + constexpr size_t num_stages = 8; + + Tableau re = { + .num_stages = num_stages, + .gamma = 0.21193756319429014, + .alpha_sum = std::make_unique_for_overwrite(num_stages), + .gamma_sum = std::make_unique_for_overwrite(num_stages), + .m = std::make_unique_for_overwrite(num_stages), + .e = {}, + .A = std::make_unique_for_overwrite(num_stages * num_stages), + .C = std::make_unique_for_overwrite(num_stages * num_stages), + .H = std::make_unique_for_overwrite(num_stages * 3), + .order = 5, + .is_krylov = false, + .is_w = false, + .is_dae = true, + }; + + re.alpha_sum[0] = 0.0; + re.alpha_sum[1] = 0.6358126895828704; + re.alpha_sum[2] = 0.4095798393397535; + re.alpha_sum[3] = 0.9769306725060716; + re.alpha_sum[4] = 0.4288403609558664; + re.alpha_sum[5] = 1.0; + re.alpha_sum[6] = 1.0; + re.alpha_sum[7] = 1.0; + + re.gamma_sum[0] = 0.21193756319429014; + re.gamma_sum[1] = -0.42387512638858027; + re.gamma_sum[2] = -0.3384627126235924; + re.gamma_sum[3] = 1.8046452872882734; + re.gamma_sum[4] = 2.325825639765069; + re.gamma_sum[5] = 0.0; + re.gamma_sum[6] = 0.0; + re.gamma_sum[7] = 0.0; + + re.m[0] = -7.502846399306121; + re.m[1] = 2.561846144803919; + re.m[2] = -11.627539656261098; + re.m[3] = -0.18268767659942256; + re.m[4] = 0.030198172008377946; + re.m[5] = 1.0; + re.m[6] = 1.0; + re.m[7] = 1.0; + + re.A[1 * 8 + 0] = 3.0; + + re.A[2 * 8 + 0] = 2.849394379747939; + re.A[2 * 8 + 1] = 0.45842242204463923; + + re.A[3 * 8 + 0] = -6.954028509809101; + re.A[3 * 8 + 1] = 2.489845061869568; + re.A[3 * 8 + 2] = -10.358996098473584; + + re.A[4 * 8 + 0] = 2.8029986275628964; + re.A[4 * 8 + 1] = 0.5072464736228206; + re.A[4 * 8 + 2] = -0.3988312541770524; + re.A[4 * 8 + 3] = -0.04721187230404641; + + re.A[5 * 8 + 0] = -7.502846399306121; + re.A[5 * 8 + 1] = 2.561846144803919; + re.A[5 * 8 + 2] = -11.627539656261098; + re.A[5 * 8 + 3] = -0.18268767659942256; + re.A[5 * 8 + 4] = 0.030198172008377946; + + re.A[6 * 8 + 0] = -7.502846399306121; + re.A[6 * 8 + 1] = 2.561846144803919; + re.A[6 * 8 + 2] = -11.627539656261098; + re.A[6 * 8 + 3] = -0.18268767659942256; + re.A[6 * 8 + 4] = 0.030198172008377946; + re.A[6 * 8 + 5] = 1.0; + + re.A[7 * 8 + 0] = -7.502846399306121; + re.A[7 * 8 + 1] = 2.561846144803919; + re.A[7 * 8 + 2] = -11.627539656261098; + re.A[7 * 8 + 3] = -0.18268767659942256; + re.A[7 * 8 + 4] = 0.030198172008377946; + re.A[7 * 8 + 5] = 1.0; + re.A[7 * 8 + 6] = 1.0; + + re.C[1 * 8 + 0] = -14.155112264123755; + + re.C[2 * 8 + 0] = -17.97296035885952; + re.C[2 * 8 + 1] = -2.859693295451294; + + re.C[3 * 8 + 0] = 147.12150275711716; + re.C[3 * 8 + 1] = -1.41221402718213; + re.C[3 * 8 + 2] = 71.68940251302358; + + re.C[4 * 8 + 0] = 165.43517024871676; + re.C[4 * 8 + 1] = -0.4592823456491126; + re.C[4 * 8 + 2] = 42.90938336958603; + re.C[4 * 8 + 3] = -5.961986721573306; + + re.C[5 * 8 + 0] = 24.854864614690072; + re.C[5 * 8 + 1] = -3.0009227002832186; + re.C[5 * 8 + 2] = 47.4931110020768; + re.C[5 * 8 + 3] = 5.5814197821558125; + re.C[5 * 8 + 4] = -0.6610691825249471; + + re.C[6 * 8 + 0] = 30.91273214028599; + re.C[6 * 8 + 1] = -3.1208243349937974; + re.C[6 * 8 + 2] = 77.79954646070892; + re.C[6 * 8 + 3] = 34.28646028294783; + re.C[6 * 8 + 4] = -19.097331116725623; + re.C[6 * 8 + 5] = -28.087943162872662; + + re.C[7 * 8 + 0] = 37.80277123390563; + re.C[7 * 8 + 1] = -3.2571969029072276; + re.C[7 * 8 + 2] = 112.26918849496327; + re.C[7 * 8 + 3] = 66.9347231244047; + re.C[7 * 8 + 4] = -40.06618937091002; + re.C[7 * 8 + 5] = -54.66780262877968; + re.C[7 * 8 + 6] = -9.48861652309627; + + re.H[0 * 8 + 0] = 25.948786856663858; + re.H[0 * 8 + 1] = -2.5579724845846235; + re.H[0 * 8 + 2] = 10.433815404888879; + re.H[0 * 8 + 3] = -2.3679251022685204; + re.H[0 * 8 + 4] = 0.524948541321073; + re.H[0 * 8 + 5] = 1.1241088310450404; + re.H[0 * 8 + 6] = 0.4272876194431874; + re.H[0 * 8 + 7] = -0.17202221070155493; + + re.H[1 * 8 + 0] = -9.91568850695171; + re.H[1 * 8 + 1] = -0.9689944594115154; + re.H[1 * 8 + 2] = 3.0438037242978453; + re.H[1 * 8 + 3] = -24.495224566215796; + re.H[1 * 8 + 4] = 20.176138334709044; + re.H[1 * 8 + 5] = 15.98066361424651; + re.H[1 * 8 + 6] = -6.789040303419874; + re.H[1 * 8 + 7] = -6.710236069923372; + + re.H[2 * 8 + 0] = 11.419903575922262; + re.H[2 * 8 + 1] = 2.8879645146136994; + re.H[2 * 8 + 2] = 72.92137995996029; + re.H[2 * 8 + 3] = 80.12511834622643; + re.H[2 * 8 + 4] = -52.072871366152654; + re.H[2 * 8 + 5] = -59.78993625266729; + re.H[2 * 8 + 6] = -0.15582684282751913; + re.H[2 * 8 + 7] = 4.883087185713722; + + return re; + } }; private: From 5f480490beaaa1770737291bfb8fcc0925d431e2 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 16 Mar 2026 18:30:54 -0400 Subject: [PATCH 11/57] Add rodas5p order test --- tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp | 8 ++++---- tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index 39e9ea715..a03decb2c 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -337,7 +337,7 @@ namespace GridKit using Rosenbrock = Integrator::Rosenbrock; public: - TestOutcome test_order(Rosenbrock::Tableau&& tab) + TestOutcome test_order(Rosenbrock::Tableau&& tab, double step_exponent_lower, double step_exponent_upper) { TestStatus success = true; @@ -361,9 +361,7 @@ namespace GridKit double final_time = 2.0; std::vector out_times = {final_time}; - double step_exponent_lower = -1.0; - double step_exponent_upper = -5.0; - size_t num_samples = 11; + size_t num_samples = 21; std::vector step_sizes; std::vector errors; @@ -387,6 +385,8 @@ namespace GridKit for (size_t i = 0; i < num_samples; i++) { double step_size = std::pow(10, step_exponent_lower + static_cast(i) * (step_exponent_upper - step_exponent_lower) / static_cast(num_samples - 1)); + double num_steps = round((final_time - 0.5) / step_size); + step_size = (final_time - 0.5) / num_steps; step_sizes.push_back(step_size); model.initialize(); diff --git a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp index a7db3cceb..34bf66138 100644 --- a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp +++ b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp @@ -9,7 +9,8 @@ int main() GridKit::Testing::TestingResults result; GridKit::Testing::RosenbrockTests test; - result += test.test_order(Integrator::Rosenbrock::Tableau::lin_implicit_euler()); + result += test.test_order(Integrator::Rosenbrock::Tableau::lin_implicit_euler(), -1.0, -5.0); + result += test.test_order(Integrator::Rosenbrock::Tableau::rodas5p(), -0.5, -2.5); return result.summary(); } From 99270a2e9d22b17efcbcfdf0d6c54207c85a4df0 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 17 Mar 2026 10:14:57 -0400 Subject: [PATCH 12/57] Add error checking --- GridKit/Solver/Dynamic/Rosenbrock.hpp | 131 +++++++++++------- .../Solver/Dynamic/RosenbrockTests.hpp | 18 ++- 2 files changed, 94 insertions(+), 55 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index b881ca213..801b5eb16 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,13 @@ #include #include +#define BUBBLE_FAIL(arg) \ + do \ + { \ + if (int err = (arg)) \ + return err; \ + } while (false) + namespace Integrator { using State = ReSolve::vector::Vector; @@ -484,6 +492,7 @@ namespace Integrator { } + [[nodiscard("May fail. Check error code.")]] int allocate() { size_t size = static_cast(model_->size()); @@ -499,16 +508,16 @@ namespace Integrator mass_ = std::make_unique(size); y_new_ = std::make_unique(size); - y_prev_->allocate(memspace_); - y_cur_->allocate(memspace_); - y_interp_->allocate(memspace_); - asum_->allocate(memspace_); - csum_->allocate(memspace_); - RHS_->allocate(memspace_); - RHS_first_stage_->allocate(memspace_); - dFdt_->allocate(memspace_); - mass_->allocate(memspace_); - y_new_->allocate(memspace_); + BUBBLE_FAIL(y_prev_->allocate(memspace_)); + BUBBLE_FAIL(y_cur_->allocate(memspace_)); + BUBBLE_FAIL(y_interp_->allocate(memspace_)); + BUBBLE_FAIL(asum_->allocate(memspace_)); + BUBBLE_FAIL(csum_->allocate(memspace_)); + BUBBLE_FAIL(RHS_->allocate(memspace_)); + BUBBLE_FAIL(RHS_first_stage_->allocate(memspace_)); + BUBBLE_FAIL(dFdt_->allocate(memspace_)); + BUBBLE_FAIL(mass_->allocate(memspace_)); + BUBBLE_FAIL(y_new_->allocate(memspace_)); if (tab_.e) { @@ -516,7 +525,7 @@ namespace Integrator if (!can_use_stage) { err_est_ = std::make_unique(size); - err_est_->allocate(memspace_); + BUBBLE_FAIL(err_est_->allocate(memspace_)); } } @@ -524,7 +533,7 @@ namespace Integrator for (size_t i = 0; i < tab_.num_stages; i++) { stages_[i] = std::make_unique(size); - stages_[i]->allocate(memspace_); + BUBBLE_FAIL(stages_[i]->allocate(memspace_)); } if (tab_.order > 2) @@ -533,7 +542,7 @@ namespace Integrator for (size_t i = 0; i < tab_.order - 2; i++) { dense_coeff_[i] = std::make_unique(size); - dense_coeff_[i]->allocate(memspace_); + BUBBLE_FAIL(dense_coeff_[i]->allocate(memspace_)); } } @@ -542,35 +551,42 @@ namespace Integrator return 0; } + [[nodiscard("May fail. Check error code.")]] int initializeSimulation(RealT t0) { current_time_ = t0; - y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_); + BUBBLE_FAIL(y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_)); jacobian_analyzed_ = false; GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); - jacobian_->setDataPointers( + BUBBLE_FAIL(jacobian_->setDataPointers( model_jacobian->getRowData(), model_jacobian->getColData(), model_jacobian->getValues(), - memspace_); - lin_solver_.setMatrix(jacobian_.get()); - lin_solver_.analyze(); - lin_solver_.preconditionerSetup(); + memspace_)); + BUBBLE_FAIL(lin_solver_.setMatrix(jacobian_.get())); + BUBBLE_FAIL(lin_solver_.analyze()); + BUBBLE_FAIL(lin_solver_.preconditionerSetup()); + + if (model_->tag().size() != static_cast(model_->size())) + { + std::cerr << "Model tag is either unset or does not match the size of the model\n"; + return 1; + } - assert(model_->tag().size() == static_cast(model_->size())); std::unique_ptr mass = std::make_unique(model_->tag().size()); for (size_t i = 0; i < static_cast(model_->size()); i++) { mass[i] = model_->tag()[i] ? 1.0 : 0.0; } - mass_->copyFromExternal(mass.get(), memspace_, memspace_); + BUBBLE_FAIL(mass_->copyFromExternal(mass.get(), memspace_, memspace_)); stats_ = Stats(); return 0; } + [[nodiscard("May fail. Check error code.")]] int integrate(const std::vector& out_times, StepController& step_controller, Parameters params = {}, @@ -593,8 +609,7 @@ namespace Integrator { while (current_time_ < out_time && stats_.num_steps < params.max_steps) { - // TODO: This assumes the step cannot fail. - time_step(current_time_, step_size_); + BUBBLE_FAIL(time_step(current_time_, step_size_)); double err = 0; @@ -685,7 +700,7 @@ namespace Integrator if (step_cb) { - y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_); + BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); model_->updateTime(current_time_, 0.0); (*step_cb)(StepInfo{ @@ -707,25 +722,25 @@ namespace Integrator { if (!dense_coefficients_valid) { - calc_dense_coeff(); + BUBBLE_FAIL(calc_dense_coeff()); dense_coefficients_valid = true; } double theta = (out_time - current_time_) / prev_step_size_ + 1; - interp_dense(theta); + BUBBLE_FAIL(interp_dense(theta)); } else { // TODO: Put code for alternative interpolation (Abdou) here double theta = (out_time - current_time_) / prev_step_size_ + 1; - y_interp_->copyFromExternal(y_prev_.get(), memspace_, memspace_); + BUBBLE_FAIL(y_interp_->copyFromExternal(y_prev_.get(), memspace_, memspace_)); vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); } if (out_cb) { - y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_); + BUBBLE_FAIL(y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_)); model_->updateTime(out_time, 0.0); (*out_cb)(out_time); @@ -733,7 +748,7 @@ namespace Integrator } else { - y_interp_->copyFromExternal(y_cur_.get(), memspace_, memspace_); + BUBBLE_FAIL(y_interp_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); break; } } @@ -758,6 +773,7 @@ namespace Integrator std::unique_ptr[]> dense_coeff_; public: + [[nodiscard("May fail. Check error code.")]] int time_step(double t0, double dt) { bool y0_copied = false; @@ -767,25 +783,25 @@ namespace Integrator [[likely]] if (!tab_.is_w || !skip_lu_) { - y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_); + BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); y0_copied = true; model_->updateTime(t0, -1.0 / (dt * tab_.gamma)); - model_->evaluateJacobian(); + BUBBLE_FAIL(model_->evaluateJacobian()); GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); - jacobian_->setDataPointers( + BUBBLE_FAIL(jacobian_->setDataPointers( model_jacobian->getRowData(), model_jacobian->getColData(), model_jacobian->getValues(), - memspace_); + memspace_)); [[likely]] if (jacobian_analyzed_) { - lin_solver_.refactorize(); + BUBBLE_FAIL(lin_solver_.refactorize()); } else { - lin_solver_.factorize(); + BUBBLE_FAIL(lin_solver_.factorize()); } stats_.jac_evals++; @@ -802,17 +818,17 @@ namespace Integrator // TODO: non-autonomous model if (!y0_copied) { - y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_); + BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); y0_copied = true; } model_->updateTime(t0, 0.0); - model_->evaluateResidual(); - RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); + BUBBLE_FAIL(model_->evaluateResidual()); + BUBBLE_FAIL(RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); vector_handler_.scal(-1, RHS_first_stage_.get(), memspace_); stats_.f_evals++; } - lin_solver_.solve(RHS_first_stage_.get(), stages_[0].get()); + BUBBLE_FAIL(lin_solver_.solve(RHS_first_stage_.get(), stages_[0].get())); stats_.decomp_solves++; // Rest of stages @@ -827,7 +843,7 @@ namespace Integrator } else { - asum_->copyFromExternal(y_cur_.get(), memspace_, memspace_); + BUBBLE_FAIL(asum_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); for (size_t j = 0; j < i; j++) { @@ -838,7 +854,7 @@ namespace Integrator // Calculate csum // TODO: Since csum is multiplied by the mass matrix, we can reduce calculations by just not calculating some indices - csum_->setToZero(memspace_); + BUBBLE_FAIL(csum_->setToZero(memspace_)); for (size_t j = 0; j < i; j++) { if (tab_.C[i * tab_.num_stages + j] != 0.0) @@ -848,16 +864,16 @@ namespace Integrator } // TODO: non-autonomous model - asum_->copyToExternal(model_->y().data(), memspace_, memspace_); + BUBBLE_FAIL(asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); model_->updateTime(t0 + tab_.alpha_sum[i] * dt, 0.0); - model_->evaluateResidual(); + BUBBLE_FAIL(model_->evaluateResidual()); RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); vector_handler_.scal(-1, RHS_.get(), memspace_); vector_handler_.scal(mass_.get(), csum_.get(), memspace_); vector_handler_.axpy(-1, csum_.get(), RHS_.get(), memspace_); - lin_solver_.solve(RHS_.get(), stages_[i].get()); + BUBBLE_FAIL(lin_solver_.solve(RHS_.get(), stages_[i].get())); stats_.f_evals++; stats_.decomp_solves++; } @@ -872,7 +888,7 @@ namespace Integrator } else { - y_new_->copyFromExternal(y_cur_.get(), memspace_, memspace_); + BUBBLE_FAIL(y_new_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); for (size_t j = 0; j < tab_.num_stages; j++) { @@ -896,7 +912,14 @@ namespace Integrator } else { - err_est_->copyFromExternal(stages_[0].get(), memspace_, memspace_); + // TODO: could make this function return recoverable errors by using std::variant + int err_code = err_est_->copyFromExternal(stages_[0].get(), memspace_, memspace_); + + if (err_code) + { + throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + } + vector_handler_.scal(tab_.e[0], stages_[0].get(), memspace_); for (size_t j = 1; j < tab_.num_stages; j++) { @@ -915,13 +938,13 @@ namespace Integrator return static_cast(tab_.H); } - void calc_dense_coeff() + int calc_dense_coeff() { if (tab_.order > 2) { for (size_t j = 0; j < tab_.order - 2; j++) { - dense_coeff_[j]->setToZero(memspace_); + BUBBLE_FAIL(dense_coeff_[j]->setToZero(memspace_)); } for (size_t i = 0; i < tab_.num_stages; i++) @@ -932,15 +955,17 @@ namespace Integrator } } } + + return 0; } - // TODO: Maybe this can be integrated into OneStepIntegrator? + [[nodiscard("May fail. Check error code.")]] int interp_dense(double theta) { double one = 1.0; if (tab_.order > 2) { - y_interp_->copyFromExternal(dense_coeff_[tab_.order - 3].get(), memspace_, memspace_); + BUBBLE_FAIL(y_interp_->copyFromExternal(dense_coeff_[tab_.order - 3].get(), memspace_, memspace_)); for (size_t i = 1; i < tab_.order - 2; i++) { @@ -950,7 +975,7 @@ namespace Integrator } else { - y_interp_->setToZero(memspace_); + BUBBLE_FAIL(y_interp_->setToZero(memspace_)); } double omt = 1 - theta; @@ -1049,4 +1074,6 @@ namespace Integrator return handler.amax(workspace_.out_.get(), memspace); } }; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator + +#undef BUBBLE_FAIL \ No newline at end of file diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index a03decb2c..f56395b03 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -354,7 +354,11 @@ namespace GridKit lin_solver.initialize(); Rosenbrock integrator(std::move(tab), &model, lin_solver, vec_handler, nullptr); - integrator.allocate(); + if (integrator.allocate()) + { + success = false; + return success.report(__func__); + } Integrator::FixedStep step_controller; @@ -390,12 +394,20 @@ namespace GridKit step_sizes.push_back(step_size); model.initialize(); - integrator.initializeSimulation(0.5); + if (integrator.initializeSimulation(0.5)) + { + success = false; + return success.report(__func__); + } typename Rosenbrock::Parameters params; params.starting_step = step_size; params.max_steps = static_cast(ceil((final_time - 0.5) / step_size)) + 10; - integrator.integrate(out_times, step_controller, params, out_cb); + if (integrator.integrate(out_times, step_controller, params, out_cb)) + { + success = false; + return success.report(__func__); + } } std::cerr << "Step sizes\n"; From 5b46fec5c73d9b477d8bc9a3f613343b8d775f02 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 17 Mar 2026 12:36:43 -0400 Subject: [PATCH 13/57] Re-organize rosenbrock implementation code --- GridKit/Solver/Dynamic/CMakeLists.txt | 1 + GridKit/Solver/Dynamic/Rosenbrock.cpp | 672 +++++++++++++- GridKit/Solver/Dynamic/Rosenbrock.hpp | 844 +----------------- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 190 ++++ 4 files changed, 886 insertions(+), 821 deletions(-) create mode 100644 GridKit/Solver/Dynamic/RosenbrockTableaus.cpp diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index 4d08eb1d0..593190bb2 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -47,6 +47,7 @@ if (GRIDKIT_ENABLE_RESOLVE) gridkit_add_library(rosenbrock SOURCES Rosenbrock.cpp + RosenbrockTableaus.cpp HEADERS ${_install_headers} LINK_LIBRARIES diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index c1b16d57e..69ab5e25a 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -1,5 +1,671 @@ #include "Rosenbrock.hpp" -namespace Integrator { - template class Rosenbrock; -} \ No newline at end of file +#include +#include +#include + +#include + +#define BUBBLE_FAIL(arg) \ + do \ + { \ + if (int err = (arg)) \ + return err; \ + } while (false) + +namespace Integrator +{ + template + std::string Rosenbrock::StepInfo::csv_report() const + { + std::stringstream out; + out << std::scientific << std::setprecision(20) + << sim_time << ',' + << step_size << ',' + << next_step_size << ',' + << err_est << ',' + << step_no << ',' + << skip_lu << ',' + << skip_f << ',' + << accepted; + + return out.str(); + } + + template + std::string Rosenbrock::StepInfo::report() const + { + std::stringstream out; + out << std::scientific << std::setprecision(20) + << "Simulation Time: " << sim_time << '\n' + << "Step Size: " << step_size << '\n' + << "Next Step Size: " << next_step_size << '\n' + << "Error Estimate: " << err_est << '\n' + << "Step Number: " << step_no << '\n' + << "Skip LU: " << skip_lu << '\n' + << "Skip F: " << skip_f << '\n' + << "Accepted: " << accepted; + + return out.str(); + } + + template + std::string Rosenbrock::Stats::report() const + { + std::stringstream out; + out << "Rejections: " << rejections.size() + << "\nSteps: " << num_steps + << "\nSkip LU Steps: " << skip_lu_steps.size() + << "\nMin Step: " << min_step + << "\nMax Step: " << max_step + << "\nRHS Function Evals: " << f_evals + << "\nRHS Function Skipped: " << f_skipped + << "\nJacobian Evals: " << jac_evals + << "\nLinear Solves: " << decomp_solves; + + return out.str(); + } + + template + Rosenbrock::Stats::Stats& Rosenbrock::Stats::operator+=(const Stats& other) + { + rejections.insert(rejections.end(), other.rejections.begin(), other.rejections.end()); + skip_lu_steps.insert(skip_lu_steps.end(), other.skip_lu_steps.begin(), other.skip_lu_steps.end()); + + num_steps += other.num_steps; + f_evals += other.f_evals; + f_skipped += other.f_skipped; + jac_evals += other.jac_evals; + decomp_solves += other.decomp_solves; + + min_step = std::min(min_step, other.min_step); + max_step = std::max(max_step, other.max_step); + + return *this; + } + + template + constexpr bool Rosenbrock::Tableau::can_reuse_asum(size_t stage) const + { + assert(stage < num_stages); + + if (stage == 0) + return false; + else + { + for (size_t j = 0; j < stage - 1; j++) + { + if (getA(stage, j) != getA(stage - 1, j)) + { + return false; + } + } + return true; + } + } + + template + constexpr bool Rosenbrock::Tableau::can_reuse_asum_for_out() const + { + if (num_stages == 1) + return false; + + for (size_t j = 0; j < num_stages - 1; j++) + { + if (getA(num_stages - 1, j) != m[j]) + { + return false; + } + } + + return true; + } + + template + constexpr std::tuple Rosenbrock::Tableau::error_estimator_stage() const + { + std::tuple re = {false, 0}; + for (size_t j = 0; j < num_stages; j++) + { + if (e[j] == 1.0 && !std::get<0>(re)) + { + re = {true, j}; + } + else if (e[j] != 0.0) + { + return {false, 0}; + } + } + return re; + } + + template + Rosenbrock::Rosenbrock(Tableau&& tab, + GridKit::Model::Evaluator* model, + ReSolve::SystemSolver& lin_solver, + ReSolve::VectorHandler& vector_handler, + const ErrorNorm* err_norm, + ReSolve::memory::MemorySpace memspace) + : tab_(std::move(tab)), + model_(model), + lin_solver_(lin_solver), + vector_handler_(vector_handler), + err_norm_(err_norm), + memspace_(memspace) + { + } + + template + int Rosenbrock::allocate() + { + size_t size = static_cast(model_->size()); + + y_prev_ = std::make_unique(size); + y_cur_ = std::make_unique(size); + y_interp_ = std::make_unique(size); + asum_ = std::make_unique(size); + csum_ = std::make_unique(size); + RHS_ = std::make_unique(size); + RHS_first_stage_ = std::make_unique(size); + dFdt_ = std::make_unique(size); + mass_ = std::make_unique(size); + y_new_ = std::make_unique(size); + + BUBBLE_FAIL(y_prev_->allocate(memspace_)); + BUBBLE_FAIL(y_cur_->allocate(memspace_)); + BUBBLE_FAIL(y_interp_->allocate(memspace_)); + BUBBLE_FAIL(asum_->allocate(memspace_)); + BUBBLE_FAIL(csum_->allocate(memspace_)); + BUBBLE_FAIL(RHS_->allocate(memspace_)); + BUBBLE_FAIL(RHS_first_stage_->allocate(memspace_)); + BUBBLE_FAIL(dFdt_->allocate(memspace_)); + BUBBLE_FAIL(mass_->allocate(memspace_)); + BUBBLE_FAIL(y_new_->allocate(memspace_)); + + if (tab_.e) + { + auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); + if (!can_use_stage) + { + err_est_ = std::make_unique(size); + BUBBLE_FAIL(err_est_->allocate(memspace_)); + } + } + + stages_ = std::make_unique[]>(tab_.num_stages); + for (size_t i = 0; i < tab_.num_stages; i++) + { + stages_[i] = std::make_unique(size); + BUBBLE_FAIL(stages_[i]->allocate(memspace_)); + } + + if (tab_.order > 2) + { + dense_coeff_ = std::make_unique[]>(tab_.order - 2); + for (size_t i = 0; i < tab_.order - 2; i++) + { + dense_coeff_[i] = std::make_unique(size); + BUBBLE_FAIL(dense_coeff_[i]->allocate(memspace_)); + } + } + + jacobian_ = std::make_unique(size, size, model_->getCsrJacobian()->getNnz()); + + return 0; + } + + template + int Rosenbrock::initializeSimulation(RealT t0) + { + current_time_ = t0; + BUBBLE_FAIL(y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_)); + jacobian_analyzed_ = false; + + GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); + BUBBLE_FAIL(jacobian_->setDataPointers( + model_jacobian->getRowData(), + model_jacobian->getColData(), + model_jacobian->getValues(), + memspace_)); + BUBBLE_FAIL(lin_solver_.setMatrix(jacobian_.get())); + BUBBLE_FAIL(lin_solver_.analyze()); + BUBBLE_FAIL(lin_solver_.preconditionerSetup()); + + if (model_->tag().size() != static_cast(model_->size())) + { + std::cerr << "Model tag is either unset or does not match the size of the model\n"; + return 1; + } + + std::unique_ptr mass = std::make_unique(model_->tag().size()); + for (size_t i = 0; i < static_cast(model_->size()); i++) + { + mass[i] = model_->tag()[i] ? 1.0 : 0.0; + } + BUBBLE_FAIL(mass_->copyFromExternal(mass.get(), memspace_, memspace_)); + + stats_ = Stats(); + + return 0; + } + + template + int Rosenbrock::integrate(const std::vector& out_times, + StepController& step_controller, + Parameters params, + std::optional> out_cb, + std::optional> step_cb) + { + skip_lu_ = false; + skip_f_ = false; + + bool prev_accept = true; + step_size_ = params.starting_step; + + double next_step_size; + + // Kahan summation time buffer. The "leftover" time that was lost when trying to add h at some point that needs to be added + // later + double time_buffer = 0; + + for (double out_time : out_times) + { + while (current_time_ < out_time && stats_.num_steps < params.max_steps) + { + BUBBLE_FAIL(time_step(current_time_, step_size_)); + + double err = 0; + + if (step_controller.usesError()) + { + if (err_norm_ == nullptr) + { + std::cerr << "The provided step controller requires the use of an error norm, but none was provided!\n"; + + return -1; + } + + State& err_vec = error_estimate(); + err = err_norm_->errorNorm(err_vec, *y_new_, *y_cur_, vector_handler_, memspace_); + } + + StepControl next_step = step_controller.nextStep(err, + StepControl{ + .accept = prev_accept, + .step_size = step_size_, + }, + tab_.order); + prev_accept = next_step.accept; + next_step_size = next_step.step_size; + + if (prev_accept) + { + // Try to add the leftover time that we've stored up + double step_size_adj = step_size_ + time_buffer; + double next_time = current_time_ + step_size_adj; + + // Kahan summation - keep track of how much of step_size_adj we weren't able to add to current_time + // due to lack of precision + time_buffer = step_size_adj - (next_time - current_time_); + current_time_ = next_time; + + // step_cb(current_time, yout); + skip_f_ = false; + + stats_.num_steps++; + if (skip_lu_) + { + stats_.skip_lu_steps.push_back(StepInfo{ + .sim_time = current_time_, + .step_size = step_size_, + .next_step_size = next_step_size, + .err_est = err, + .step_no = stats_.num_steps, + .skip_lu = skip_lu_, + .skip_f = skip_f_, + .accepted = prev_accept, + }); + } + stats_.min_step = std::min(stats_.min_step, step_size_); + stats_.max_step = std::max(stats_.max_step, step_size_); + + std::swap(y_prev_, y_cur_); + std::swap(y_cur_, y_new_); + dense_coefficients_valid = false; + } + else + { + skip_f_ = true; + + stats_.rejections.push_back(StepInfo{ + .sim_time = current_time_, + .step_size = step_size_, + .next_step_size = next_step_size, + .err_est = err, + .step_no = stats_.num_steps, + .skip_lu = skip_lu_, + .skip_f = skip_f_, + .accepted = prev_accept, + }); + } + + double step_gain = next_step_size / step_size_; + if (params.skip_lu && step_gain >= 1 && step_gain <= 1.2) + { + skip_lu_ = true; + } + else + { + skip_lu_ = false; + prev_step_size_ = step_size_; + step_size_ = next_step_size; + } + + if (step_cb) + { + BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); + model_->updateTime(current_time_, 0.0); + + (*step_cb)(StepInfo{ + .sim_time = current_time_, + .step_size = step_size_, + .next_step_size = next_step_size, + .err_est = err, + .step_no = stats_.num_steps, + .skip_lu = skip_lu_, + .skip_f = skip_f_, + .accepted = prev_accept, + }); + } + } + + if (current_time_ >= out_time) + { + if (tab_.hasDenseOutput()) + { + if (!dense_coefficients_valid) + { + BUBBLE_FAIL(calc_dense_coeff()); + dense_coefficients_valid = true; + } + + double theta = (out_time - current_time_) / prev_step_size_ + 1; + BUBBLE_FAIL(interp_dense(theta)); + } + else + { + // TODO: Put code for alternative interpolation (Abdou) here + double theta = (out_time - current_time_) / prev_step_size_ + 1; + BUBBLE_FAIL(y_interp_->copyFromExternal(y_prev_.get(), memspace_, memspace_)); + vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); + vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); + } + + if (out_cb) + { + BUBBLE_FAIL(y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_)); + model_->updateTime(out_time, 0.0); + + (*out_cb)(out_time); + } + } + else + { + BUBBLE_FAIL(y_interp_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); + break; + } + } + + return 0; + } + + template + int Rosenbrock::time_step(double t0, double dt) + { + bool y0_copied = false; + + // Form the left-hand side of the system + // Can sometimes be skipped if the method is a w-method + [[likely]] + if (!tab_.is_w || !skip_lu_) + { + BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); + y0_copied = true; + model_->updateTime(t0, -1.0 / (dt * tab_.gamma)); + BUBBLE_FAIL(model_->evaluateJacobian()); + GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); + BUBBLE_FAIL(jacobian_->setDataPointers( + model_jacobian->getRowData(), + model_jacobian->getColData(), + model_jacobian->getValues(), + memspace_)); + + [[likely]] + if (jacobian_analyzed_) + { + BUBBLE_FAIL(lin_solver_.refactorize()); + } + else + { + BUBBLE_FAIL(lin_solver_.factorize()); + } + + stats_.jac_evals++; + } + + // First stage + [[unlikely]] + if (skip_f_) + { + stats_.f_skipped++; + } + else + { + // TODO: non-autonomous model + if (!y0_copied) + { + BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); + y0_copied = true; + } + model_->updateTime(t0, 0.0); + BUBBLE_FAIL(model_->evaluateResidual()); + BUBBLE_FAIL(RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); + vector_handler_.scal(-1, RHS_first_stage_.get(), memspace_); + + stats_.f_evals++; + } + BUBBLE_FAIL(lin_solver_.solve(RHS_first_stage_.get(), stages_[0].get())); + stats_.decomp_solves++; + + // Rest of stages + for (size_t i = 1; i < tab_.num_stages; i++) + { + // Calculate asum + // We can sometimes reuse asum from the previous stage + if (i > 1 && tab_.can_reuse_asum(i)) + { + if (tab_.A[tab_.num_stages * i + i - 1] != 0.0) + vector_handler_.axpy(tab_.A[tab_.num_stages * i + i - 1], stages_[i - 1].get(), asum_.get(), memspace_); + } + else + { + BUBBLE_FAIL(asum_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); + + for (size_t j = 0; j < i; j++) + { + if (tab_.A[tab_.num_stages * i + j] != 0.0) + vector_handler_.axpy(tab_.A[tab_.num_stages * i + j], stages_[j].get(), asum_.get(), memspace_); + } + } + + // Calculate csum + // TODO: Since csum is multiplied by the mass matrix, we can reduce calculations by just not calculating some indices + BUBBLE_FAIL(csum_->setToZero(memspace_)); + for (size_t j = 0; j < i; j++) + { + if (tab_.C[i * tab_.num_stages + j] != 0.0) + { + vector_handler_.axpy(tab_.C[i * tab_.num_stages + j] / dt, stages_[j].get(), csum_.get(), memspace_); + } + } + + // TODO: non-autonomous model + BUBBLE_FAIL(asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); + model_->updateTime(t0 + tab_.alpha_sum[i] * dt, 0.0); + BUBBLE_FAIL(model_->evaluateResidual()); + RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); + + vector_handler_.scal(-1, RHS_.get(), memspace_); + vector_handler_.scal(mass_.get(), csum_.get(), memspace_); + vector_handler_.axpy(-1, csum_.get(), RHS_.get(), memspace_); + + BUBBLE_FAIL(lin_solver_.solve(RHS_.get(), stages_[i].get())); + stats_.f_evals++; + stats_.decomp_solves++; + } + + // Compute the solution at time t + dt + // It happens often where the solution is just asum of the last stage + // plus some multiple of the last stage. In that case we can avoid a matmul + if (tab_.can_reuse_asum_for_out()) + { + std::swap(asum_, y_new_); + vector_handler_.axpy(tab_.m[tab_.num_stages - 1], stages_[tab_.num_stages - 1].get(), y_new_.get(), memspace_); + } + else + { + BUBBLE_FAIL(y_new_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); + + for (size_t j = 0; j < tab_.num_stages; j++) + { + if (tab_.m[j] != 0.0) + { + vector_handler_.axpy(tab_.m[j], stages_[j].get(), y_new_.get(), memspace_); + } + } + } + + return 0; + } + + template + State& Rosenbrock::error_estimate() const + { + auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); + + if (can_use_stage) + { + return *stages_[err_stage]; + } + else + { + // TODO: could make this function return recoverable errors by using std::variant + int err_code = err_est_->copyFromExternal(stages_[0].get(), memspace_, memspace_); + + if (err_code) + { + throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + } + + vector_handler_.scal(tab_.e[0], stages_[0].get(), memspace_); + for (size_t j = 1; j < tab_.num_stages; j++) + { + if (tab_.e[j] != 0.0) + { + vector_handler_.axpy(tab_.e[j], stages_[j].get(), err_est_.get(), memspace_); + } + } + + return *err_est_; + } + } + + template + int Rosenbrock::calc_dense_coeff() + { + if (tab_.order > 2) + { + for (size_t j = 0; j < tab_.order - 2; j++) + { + BUBBLE_FAIL(dense_coeff_[j]->setToZero(memspace_)); + } + + for (size_t i = 0; i < tab_.num_stages; i++) + { + for (size_t j = 0; j < tab_.order - 2; j++) + { + vector_handler_.axpy(tab_.H[j * tab_.num_stages + i], stages_[i].get(), dense_coeff_[j].get(), memspace_); + } + } + } + + return 0; + } + + template + int Rosenbrock::interp_dense(double theta) + { + double one = 1.0; + if (tab_.order > 2) + { + BUBBLE_FAIL(y_interp_->copyFromExternal(dense_coeff_[tab_.order - 3].get(), memspace_, memspace_)); + + for (size_t i = 1; i < tab_.order - 2; i++) + { + vector_handler_.scal(theta, y_interp_.get(), memspace_); + vector_handler_.axpy(one, dense_coeff_[tab_.order - 3 - i].get(), y_interp_.get(), memspace_); + } + } + else + { + BUBBLE_FAIL(y_interp_->setToZero(memspace_)); + } + + double omt = 1 - theta; + + vector_handler_.scal(omt, y_interp_.get(), memspace_); + vector_handler_.axpy(one, y_cur_.get(), y_interp_.get(), memspace_); + vector_handler_.scal(theta, y_interp_.get(), memspace_); + vector_handler_.axpy(omt, y_prev_.get(), y_interp_.get(), memspace_); + + return 0; + } + + StepControl AdaptiveStep::nextStep(double err, StepControl prev_step, uint8_t method_order) + { + StepControl next_step = prev_step; + + double h_mult = std::min(params_.facmax, std::max(params_.facscale * std::pow(err, -1.0 / method_order), params_.facmin)); + + next_step.accept = err <= 1; + next_step.step_size *= h_mult; + + return next_step; + } + + StepControl FixedStep::nextStep([[maybe_unused]] double err, [[maybe_unused]] StepControl prev_step, [[maybe_unused]] uint8_t method_order) + { + return StepControl{ + .accept = true, + .step_size = prev_step.step_size, + }; + } + + double InfNorm::errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const + { + workspace_.out_->copyFromExternal(&err, memspace, memspace); + workspace_.scale_->copyFromExternal(&y, memspace, memspace); + workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace); + + handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); + handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); + handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.yprev_abs_.get(), memspace); + handler.scal(params_.rtol, workspace_.scale_.get(), memspace); + handler.axpy(1.0, params_.atol.get(), workspace_.scale_.get(), memspace); + handler.scaleInv(workspace_.scale_.get(), workspace_.out_.get(), memspace); + + return handler.amax(workspace_.out_.get(), memspace); + } + + template class Rosenbrock; +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 801b5eb16..33b56ffd1 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -3,18 +3,13 @@ #include #include #include -#include -#include -#include #include #include -#include #include #include -#include +#include -#include "GridKit/Model/Evaluator.hpp" #include #include #include @@ -23,13 +18,6 @@ #include #include -#define BUBBLE_FAIL(arg) \ - do \ - { \ - if (int err = (arg)) \ - return err; \ - } while (false) - namespace Integrator { using State = ReSolve::vector::Vector; @@ -43,8 +31,8 @@ namespace Integrator class StepController { public: - virtual constexpr StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) = 0; - virtual constexpr bool usesError() const = 0; + virtual StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) = 0; + virtual bool usesError() const = 0; }; class ErrorNorm @@ -70,37 +58,8 @@ namespace Integrator bool skip_f; bool accepted; - std::string csv_report() const - { - std::stringstream out; - out << std::scientific << std::setprecision(20) - << sim_time << ',' - << step_size << ',' - << next_step_size << ',' - << err_est << ',' - << step_no << ',' - << skip_lu << ',' - << skip_f << ',' - << accepted; - - return out.str(); - } - - std::string report() const - { - std::stringstream out; - out << std::scientific << std::setprecision(20) - << "Simulation Time: " << sim_time << '\n' - << "Step Size: " << step_size << '\n' - << "Next Step Size: " << next_step_size << '\n' - << "Error Estimate: " << err_est << '\n' - << "Step Number: " << step_no << '\n' - << "Skip LU: " << skip_lu << '\n' - << "Skip F: " << skip_f << '\n' - << "Accepted: " << accepted; - - return out.str(); - } + std::string csv_report() const; + std::string report() const; }; struct Stats @@ -115,38 +74,8 @@ namespace Integrator double min_step = INFINITY; double max_step = 0; - std::string report() const - { - std::stringstream out; - out << "Rejections: " << rejections.size() - << "\nSteps: " << num_steps - << "\nSkip LU Steps: " << skip_lu_steps.size() - << "\nMin Step: " << min_step - << "\nMax Step: " << max_step - << "\nRHS Function Evals: " << f_evals - << "\nRHS Function Skipped: " << f_skipped - << "\nJacobian Evals: " << jac_evals - << "\nLinear Solves: " << decomp_solves; - - return out.str(); - } - - Stats& operator+=(const Stats& other) - { - rejections.insert(rejections.end(), other.rejections.begin(), other.rejections.end()); - skip_lu_steps.insert(skip_lu_steps.end(), other.skip_lu_steps.begin(), other.skip_lu_steps.end()); - - num_steps += other.num_steps; - f_evals += other.f_evals; - f_skipped += other.f_skipped; - jac_evals += other.jac_evals; - decomp_solves += other.decomp_solves; - - min_step = std::min(min_step, other.min_step); - max_step = std::max(max_step, other.max_step); - - return *this; - } + std::string report() const; + Stats& operator+=(const Stats& other); }; struct Parameters @@ -200,7 +129,7 @@ namespace Integrator /// @brief Whether or not this tableau contains an embedded method constexpr bool has_embedded() const { - return e == nullptr; + return static_cast(e); } constexpr bool hasDenseOutput() const @@ -213,237 +142,12 @@ namespace Integrator return A[row * num_stages + col]; } - constexpr bool can_reuse_asum(size_t stage) const - { - assert(stage < num_stages); - - if (stage == 0) - return false; - else - { - for (size_t j = 0; j < stage - 1; j++) - { - if (getA(stage, j) != getA(stage - 1, j)) - { - return false; - } - } - return true; - } - } - - constexpr bool can_reuse_asum_for_out() const - { - if (num_stages == 1) - return false; - - for (size_t j = 0; j < num_stages - 1; j++) - { - if (getA(num_stages - 1, j) != m[j]) - { - return false; - } - } - - return true; - } - - constexpr std::tuple error_estimator_stage() const - { - std::tuple re = {false, 0}; - for (size_t j = 0; j < num_stages; j++) - { - if (e[j] == 1.0 && !std::get<0>(re)) - { - re = {true, j}; - } - else if (e[j] != 0.0) - { - return {false, 0}; - } - } - return re; - } - - static constexpr Tableau lin_implicit_euler() - { - constexpr size_t num_stages = 1; - - Tableau re = { - .num_stages = num_stages, - .gamma = 1.0, - .alpha_sum = std::make_unique_for_overwrite(num_stages), - .gamma_sum = std::make_unique_for_overwrite(num_stages), - .m = std::make_unique_for_overwrite(num_stages), - .e = {}, - .A = std::make_unique_for_overwrite(num_stages * num_stages), - .C = std::make_unique_for_overwrite(num_stages * num_stages), - .H = {}, - .order = 1, - .is_krylov = true, - .is_w = false, - .is_dae = true, - }; - - re.alpha_sum[0] = 0.0; - - re.gamma_sum[0] = 1.0; - - re.m[0] = 1.0; - - re.A[0] = 0.0; - - re.C[0] = 2.0; - - return re; - } - - static constexpr Tableau rodas5p() - { - constexpr size_t num_stages = 8; - - Tableau re = { - .num_stages = num_stages, - .gamma = 0.21193756319429014, - .alpha_sum = std::make_unique_for_overwrite(num_stages), - .gamma_sum = std::make_unique_for_overwrite(num_stages), - .m = std::make_unique_for_overwrite(num_stages), - .e = {}, - .A = std::make_unique_for_overwrite(num_stages * num_stages), - .C = std::make_unique_for_overwrite(num_stages * num_stages), - .H = std::make_unique_for_overwrite(num_stages * 3), - .order = 5, - .is_krylov = false, - .is_w = false, - .is_dae = true, - }; - - re.alpha_sum[0] = 0.0; - re.alpha_sum[1] = 0.6358126895828704; - re.alpha_sum[2] = 0.4095798393397535; - re.alpha_sum[3] = 0.9769306725060716; - re.alpha_sum[4] = 0.4288403609558664; - re.alpha_sum[5] = 1.0; - re.alpha_sum[6] = 1.0; - re.alpha_sum[7] = 1.0; - - re.gamma_sum[0] = 0.21193756319429014; - re.gamma_sum[1] = -0.42387512638858027; - re.gamma_sum[2] = -0.3384627126235924; - re.gamma_sum[3] = 1.8046452872882734; - re.gamma_sum[4] = 2.325825639765069; - re.gamma_sum[5] = 0.0; - re.gamma_sum[6] = 0.0; - re.gamma_sum[7] = 0.0; - - re.m[0] = -7.502846399306121; - re.m[1] = 2.561846144803919; - re.m[2] = -11.627539656261098; - re.m[3] = -0.18268767659942256; - re.m[4] = 0.030198172008377946; - re.m[5] = 1.0; - re.m[6] = 1.0; - re.m[7] = 1.0; - - re.A[1 * 8 + 0] = 3.0; - - re.A[2 * 8 + 0] = 2.849394379747939; - re.A[2 * 8 + 1] = 0.45842242204463923; - - re.A[3 * 8 + 0] = -6.954028509809101; - re.A[3 * 8 + 1] = 2.489845061869568; - re.A[3 * 8 + 2] = -10.358996098473584; - - re.A[4 * 8 + 0] = 2.8029986275628964; - re.A[4 * 8 + 1] = 0.5072464736228206; - re.A[4 * 8 + 2] = -0.3988312541770524; - re.A[4 * 8 + 3] = -0.04721187230404641; + constexpr bool can_reuse_asum(size_t stage) const; + constexpr bool can_reuse_asum_for_out() const; + constexpr std::tuple error_estimator_stage() const; - re.A[5 * 8 + 0] = -7.502846399306121; - re.A[5 * 8 + 1] = 2.561846144803919; - re.A[5 * 8 + 2] = -11.627539656261098; - re.A[5 * 8 + 3] = -0.18268767659942256; - re.A[5 * 8 + 4] = 0.030198172008377946; - - re.A[6 * 8 + 0] = -7.502846399306121; - re.A[6 * 8 + 1] = 2.561846144803919; - re.A[6 * 8 + 2] = -11.627539656261098; - re.A[6 * 8 + 3] = -0.18268767659942256; - re.A[6 * 8 + 4] = 0.030198172008377946; - re.A[6 * 8 + 5] = 1.0; - - re.A[7 * 8 + 0] = -7.502846399306121; - re.A[7 * 8 + 1] = 2.561846144803919; - re.A[7 * 8 + 2] = -11.627539656261098; - re.A[7 * 8 + 3] = -0.18268767659942256; - re.A[7 * 8 + 4] = 0.030198172008377946; - re.A[7 * 8 + 5] = 1.0; - re.A[7 * 8 + 6] = 1.0; - - re.C[1 * 8 + 0] = -14.155112264123755; - - re.C[2 * 8 + 0] = -17.97296035885952; - re.C[2 * 8 + 1] = -2.859693295451294; - - re.C[3 * 8 + 0] = 147.12150275711716; - re.C[3 * 8 + 1] = -1.41221402718213; - re.C[3 * 8 + 2] = 71.68940251302358; - - re.C[4 * 8 + 0] = 165.43517024871676; - re.C[4 * 8 + 1] = -0.4592823456491126; - re.C[4 * 8 + 2] = 42.90938336958603; - re.C[4 * 8 + 3] = -5.961986721573306; - - re.C[5 * 8 + 0] = 24.854864614690072; - re.C[5 * 8 + 1] = -3.0009227002832186; - re.C[5 * 8 + 2] = 47.4931110020768; - re.C[5 * 8 + 3] = 5.5814197821558125; - re.C[5 * 8 + 4] = -0.6610691825249471; - - re.C[6 * 8 + 0] = 30.91273214028599; - re.C[6 * 8 + 1] = -3.1208243349937974; - re.C[6 * 8 + 2] = 77.79954646070892; - re.C[6 * 8 + 3] = 34.28646028294783; - re.C[6 * 8 + 4] = -19.097331116725623; - re.C[6 * 8 + 5] = -28.087943162872662; - - re.C[7 * 8 + 0] = 37.80277123390563; - re.C[7 * 8 + 1] = -3.2571969029072276; - re.C[7 * 8 + 2] = 112.26918849496327; - re.C[7 * 8 + 3] = 66.9347231244047; - re.C[7 * 8 + 4] = -40.06618937091002; - re.C[7 * 8 + 5] = -54.66780262877968; - re.C[7 * 8 + 6] = -9.48861652309627; - - re.H[0 * 8 + 0] = 25.948786856663858; - re.H[0 * 8 + 1] = -2.5579724845846235; - re.H[0 * 8 + 2] = 10.433815404888879; - re.H[0 * 8 + 3] = -2.3679251022685204; - re.H[0 * 8 + 4] = 0.524948541321073; - re.H[0 * 8 + 5] = 1.1241088310450404; - re.H[0 * 8 + 6] = 0.4272876194431874; - re.H[0 * 8 + 7] = -0.17202221070155493; - - re.H[1 * 8 + 0] = -9.91568850695171; - re.H[1 * 8 + 1] = -0.9689944594115154; - re.H[1 * 8 + 2] = 3.0438037242978453; - re.H[1 * 8 + 3] = -24.495224566215796; - re.H[1 * 8 + 4] = 20.176138334709044; - re.H[1 * 8 + 5] = 15.98066361424651; - re.H[1 * 8 + 6] = -6.789040303419874; - re.H[1 * 8 + 7] = -6.710236069923372; - - re.H[2 * 8 + 0] = 11.419903575922262; - re.H[2 * 8 + 1] = 2.8879645146136994; - re.H[2 * 8 + 2] = 72.92137995996029; - re.H[2 * 8 + 3] = 80.12511834622643; - re.H[2 * 8 + 4] = -52.072871366152654; - re.H[2 * 8 + 5] = -59.78993625266729; - re.H[2 * 8 + 6] = -0.15582684282751913; - re.H[2 * 8 + 7] = 4.883087185713722; - - return re; - } + static Tableau lin_implicit_euler(); + static Tableau rodas5p(); }; private: @@ -482,279 +186,20 @@ namespace Integrator ReSolve::SystemSolver& lin_solver, ReSolve::VectorHandler& vector_handler, const ErrorNorm* err_norm, - ReSolve::memory::MemorySpace memspace = ReSolve::memory::HOST) - : tab_(std::move(tab)), - model_(model), - lin_solver_(lin_solver), - vector_handler_(vector_handler), - err_norm_(err_norm), - memspace_(memspace) - { - } + ReSolve::memory::MemorySpace memspace = ReSolve::memory::HOST); [[nodiscard("May fail. Check error code.")]] - int allocate() - { - size_t size = static_cast(model_->size()); - - y_prev_ = std::make_unique(size); - y_cur_ = std::make_unique(size); - y_interp_ = std::make_unique(size); - asum_ = std::make_unique(size); - csum_ = std::make_unique(size); - RHS_ = std::make_unique(size); - RHS_first_stage_ = std::make_unique(size); - dFdt_ = std::make_unique(size); - mass_ = std::make_unique(size); - y_new_ = std::make_unique(size); - - BUBBLE_FAIL(y_prev_->allocate(memspace_)); - BUBBLE_FAIL(y_cur_->allocate(memspace_)); - BUBBLE_FAIL(y_interp_->allocate(memspace_)); - BUBBLE_FAIL(asum_->allocate(memspace_)); - BUBBLE_FAIL(csum_->allocate(memspace_)); - BUBBLE_FAIL(RHS_->allocate(memspace_)); - BUBBLE_FAIL(RHS_first_stage_->allocate(memspace_)); - BUBBLE_FAIL(dFdt_->allocate(memspace_)); - BUBBLE_FAIL(mass_->allocate(memspace_)); - BUBBLE_FAIL(y_new_->allocate(memspace_)); - - if (tab_.e) - { - auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); - if (!can_use_stage) - { - err_est_ = std::make_unique(size); - BUBBLE_FAIL(err_est_->allocate(memspace_)); - } - } - - stages_ = std::make_unique[]>(tab_.num_stages); - for (size_t i = 0; i < tab_.num_stages; i++) - { - stages_[i] = std::make_unique(size); - BUBBLE_FAIL(stages_[i]->allocate(memspace_)); - } - - if (tab_.order > 2) - { - dense_coeff_ = std::make_unique[]>(tab_.order - 2); - for (size_t i = 0; i < tab_.order - 2; i++) - { - dense_coeff_[i] = std::make_unique(size); - BUBBLE_FAIL(dense_coeff_[i]->allocate(memspace_)); - } - } - - jacobian_ = std::make_unique(size, size, model_->getCsrJacobian()->getNnz()); - - return 0; - } + int allocate(); [[nodiscard("May fail. Check error code.")]] - int initializeSimulation(RealT t0) - { - current_time_ = t0; - BUBBLE_FAIL(y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_)); - jacobian_analyzed_ = false; - - GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); - BUBBLE_FAIL(jacobian_->setDataPointers( - model_jacobian->getRowData(), - model_jacobian->getColData(), - model_jacobian->getValues(), - memspace_)); - BUBBLE_FAIL(lin_solver_.setMatrix(jacobian_.get())); - BUBBLE_FAIL(lin_solver_.analyze()); - BUBBLE_FAIL(lin_solver_.preconditionerSetup()); - - if (model_->tag().size() != static_cast(model_->size())) - { - std::cerr << "Model tag is either unset or does not match the size of the model\n"; - return 1; - } - - std::unique_ptr mass = std::make_unique(model_->tag().size()); - for (size_t i = 0; i < static_cast(model_->size()); i++) - { - mass[i] = model_->tag()[i] ? 1.0 : 0.0; - } - BUBBLE_FAIL(mass_->copyFromExternal(mass.get(), memspace_, memspace_)); - - stats_ = Stats(); - - return 0; - } + int initializeSimulation(RealT t0); [[nodiscard("May fail. Check error code.")]] int integrate(const std::vector& out_times, StepController& step_controller, Parameters params = {}, std::optional> out_cb = {}, - std::optional> step_cb = {}) - { - skip_lu_ = false; - skip_f_ = false; - - bool prev_accept = true; - step_size_ = params.starting_step; - - double next_step_size; - - // Kahan summation time buffer. The "leftover" time that was lost when trying to add h at some point that needs to be added - // later - double time_buffer = 0; - - for (double out_time : out_times) - { - while (current_time_ < out_time && stats_.num_steps < params.max_steps) - { - BUBBLE_FAIL(time_step(current_time_, step_size_)); - - double err = 0; - - if (step_controller.usesError()) - { - if (err_norm_ == nullptr) - { - std::cerr << "The provided step controller requires the use of an error norm, but none was provided!\n"; - - return -1; - } - - State& err_vec = error_estimate(); - err = err_norm_->errorNorm(err_vec, *y_new_, *y_cur_, vector_handler_, memspace_); - } - - StepControl next_step = step_controller.nextStep(err, - StepControl{ - .accept = prev_accept, - .step_size = step_size_, - }, - tab_.order); - prev_accept = next_step.accept; - next_step_size = next_step.step_size; - - if (prev_accept) - { - // Try to add the leftover time that we've stored up - double step_size_adj = step_size_ + time_buffer; - double next_time = current_time_ + step_size_adj; - - // Kahan summation - keep track of how much of step_size_adj we weren't able to add to current_time - // due to lack of precision - time_buffer = step_size_adj - (next_time - current_time_); - current_time_ = next_time; - - // step_cb(current_time, yout); - skip_f_ = false; - - stats_.num_steps++; - if (skip_lu_) - { - stats_.skip_lu_steps.push_back(StepInfo{ - .sim_time = current_time_, - .step_size = step_size_, - .next_step_size = next_step_size, - .err_est = err, - .step_no = stats_.num_steps, - .skip_lu = skip_lu_, - .skip_f = skip_f_, - .accepted = prev_accept, - }); - } - stats_.min_step = std::min(stats_.min_step, step_size_); - stats_.max_step = std::max(stats_.max_step, step_size_); - - std::swap(y_prev_, y_cur_); - std::swap(y_cur_, y_new_); - dense_coefficients_valid = false; - } - else - { - skip_f_ = true; - - stats_.rejections.push_back(StepInfo{ - .sim_time = current_time_, - .step_size = step_size_, - .next_step_size = next_step_size, - .err_est = err, - .step_no = stats_.num_steps, - .skip_lu = skip_lu_, - .skip_f = skip_f_, - .accepted = prev_accept, - }); - } - - double step_gain = next_step_size / step_size_; - if (params.skip_lu && step_gain >= 1 && step_gain <= 1.2) - { - skip_lu_ = true; - } - else - { - skip_lu_ = false; - prev_step_size_ = step_size_; - step_size_ = next_step_size; - } - - if (step_cb) - { - BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(current_time_, 0.0); - - (*step_cb)(StepInfo{ - .sim_time = current_time_, - .step_size = step_size_, - .next_step_size = next_step_size, - .err_est = err, - .step_no = stats_.num_steps, - .skip_lu = skip_lu_, - .skip_f = skip_f_, - .accepted = prev_accept, - }); - } - } - - if (current_time_ >= out_time) - { - if (tab_.hasDenseOutput()) - { - if (!dense_coefficients_valid) - { - BUBBLE_FAIL(calc_dense_coeff()); - dense_coefficients_valid = true; - } - - double theta = (out_time - current_time_) / prev_step_size_ + 1; - BUBBLE_FAIL(interp_dense(theta)); - } - else - { - // TODO: Put code for alternative interpolation (Abdou) here - double theta = (out_time - current_time_) / prev_step_size_ + 1; - BUBBLE_FAIL(y_interp_->copyFromExternal(y_prev_.get(), memspace_, memspace_)); - vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); - vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); - } - - if (out_cb) - { - BUBBLE_FAIL(y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(out_time, 0.0); - - (*out_cb)(out_time); - } - } - else - { - BUBBLE_FAIL(y_interp_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); - break; - } - } - - return 0; - } + std::optional> step_cb = {}); private: std::unique_ptr asum_; @@ -774,219 +219,15 @@ namespace Integrator public: [[nodiscard("May fail. Check error code.")]] - int time_step(double t0, double dt) - { - bool y0_copied = false; - - // Form the left-hand side of the system - // Can sometimes be skipped if the method is a w-method - [[likely]] - if (!tab_.is_w || !skip_lu_) - { - BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); - y0_copied = true; - model_->updateTime(t0, -1.0 / (dt * tab_.gamma)); - BUBBLE_FAIL(model_->evaluateJacobian()); - GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); - BUBBLE_FAIL(jacobian_->setDataPointers( - model_jacobian->getRowData(), - model_jacobian->getColData(), - model_jacobian->getValues(), - memspace_)); - - [[likely]] - if (jacobian_analyzed_) - { - BUBBLE_FAIL(lin_solver_.refactorize()); - } - else - { - BUBBLE_FAIL(lin_solver_.factorize()); - } - - stats_.jac_evals++; - } - - // First stage - [[unlikely]] - if (skip_f_) - { - stats_.f_skipped++; - } - else - { - // TODO: non-autonomous model - if (!y0_copied) - { - BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); - y0_copied = true; - } - model_->updateTime(t0, 0.0); - BUBBLE_FAIL(model_->evaluateResidual()); - BUBBLE_FAIL(RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); - vector_handler_.scal(-1, RHS_first_stage_.get(), memspace_); - - stats_.f_evals++; - } - BUBBLE_FAIL(lin_solver_.solve(RHS_first_stage_.get(), stages_[0].get())); - stats_.decomp_solves++; - - // Rest of stages - for (size_t i = 1; i < tab_.num_stages; i++) - { - // Calculate asum - // We can sometimes reuse asum from the previous stage - if (i > 1 && tab_.can_reuse_asum(i)) - { - if (tab_.A[tab_.num_stages * i + i - 1] != 0.0) - vector_handler_.axpy(tab_.A[tab_.num_stages * i + i - 1], stages_[i - 1].get(), asum_.get(), memspace_); - } - else - { - BUBBLE_FAIL(asum_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); - - for (size_t j = 0; j < i; j++) - { - if (tab_.A[tab_.num_stages * i + j] != 0.0) - vector_handler_.axpy(tab_.A[tab_.num_stages * i + j], stages_[j].get(), asum_.get(), memspace_); - } - } - - // Calculate csum - // TODO: Since csum is multiplied by the mass matrix, we can reduce calculations by just not calculating some indices - BUBBLE_FAIL(csum_->setToZero(memspace_)); - for (size_t j = 0; j < i; j++) - { - if (tab_.C[i * tab_.num_stages + j] != 0.0) - { - vector_handler_.axpy(tab_.C[i * tab_.num_stages + j] / dt, stages_[j].get(), csum_.get(), memspace_); - } - } - - // TODO: non-autonomous model - BUBBLE_FAIL(asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(t0 + tab_.alpha_sum[i] * dt, 0.0); - BUBBLE_FAIL(model_->evaluateResidual()); - RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); - - vector_handler_.scal(-1, RHS_.get(), memspace_); - vector_handler_.scal(mass_.get(), csum_.get(), memspace_); - vector_handler_.axpy(-1, csum_.get(), RHS_.get(), memspace_); - - BUBBLE_FAIL(lin_solver_.solve(RHS_.get(), stages_[i].get())); - stats_.f_evals++; - stats_.decomp_solves++; - } - - // Compute the solution at time t + dt - // It happens often where the solution is just asum of the last stage - // plus some multiple of the last stage. In that case we can avoid a matmul - if (tab_.can_reuse_asum_for_out()) - { - std::swap(asum_, y_new_); - vector_handler_.axpy(tab_.m[tab_.num_stages - 1], stages_[tab_.num_stages - 1].get(), y_new_.get(), memspace_); - } - else - { - BUBBLE_FAIL(y_new_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); + int time_step(double t0, double dt); - for (size_t j = 0; j < tab_.num_stages; j++) - { - if (tab_.m[j] != 0.0) - { - vector_handler_.axpy(tab_.m[j], stages_[j].get(), y_new_.get(), memspace_); - } - } - } - - return 0; - } - - State& error_estimate() const - { - auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); - - if (can_use_stage) - { - return *stages_[err_stage]; - } - else - { - // TODO: could make this function return recoverable errors by using std::variant - int err_code = err_est_->copyFromExternal(stages_[0].get(), memspace_, memspace_); - - if (err_code) - { - throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); - } - - vector_handler_.scal(tab_.e[0], stages_[0].get(), memspace_); - for (size_t j = 1; j < tab_.num_stages; j++) - { - if (tab_.e[j] != 0.0) - { - vector_handler_.axpy(tab_.e[j], stages_[j].get(), err_est_.get(), memspace_); - } - } - - return *err_est_; - } - } - - constexpr bool has_dense_output() - { - return static_cast(tab_.H); - } - - int calc_dense_coeff() - { - if (tab_.order > 2) - { - for (size_t j = 0; j < tab_.order - 2; j++) - { - BUBBLE_FAIL(dense_coeff_[j]->setToZero(memspace_)); - } - - for (size_t i = 0; i < tab_.num_stages; i++) - { - for (size_t j = 0; j < tab_.order - 2; j++) - { - vector_handler_.axpy(tab_.H[j * tab_.num_stages + i], stages_[i].get(), dense_coeff_[j].get(), memspace_); - } - } - } - - return 0; - } + State& error_estimate() const; [[nodiscard("May fail. Check error code.")]] - int interp_dense(double theta) - { - double one = 1.0; - if (tab_.order > 2) - { - BUBBLE_FAIL(y_interp_->copyFromExternal(dense_coeff_[tab_.order - 3].get(), memspace_, memspace_)); - - for (size_t i = 1; i < tab_.order - 2; i++) - { - vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(one, dense_coeff_[tab_.order - 3 - i].get(), y_interp_.get(), memspace_); - } - } - else - { - BUBBLE_FAIL(y_interp_->setToZero(memspace_)); - } + int calc_dense_coeff(); - double omt = 1 - theta; - - vector_handler_.scal(omt, y_interp_.get(), memspace_); - vector_handler_.axpy(one, y_cur_.get(), y_interp_.get(), memspace_); - vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(omt, y_prev_.get(), y_interp_.get(), memspace_); - - return 0; - } + [[nodiscard("May fail. Check error code.")]] + int interp_dense(double theta); }; class AdaptiveStep : public StepController @@ -1003,17 +244,7 @@ namespace Integrator { } - constexpr StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) final - { - StepControl next_step = prev_step; - - double h_mult = std::min(params_.facmax, std::max(params_.facscale * std::pow(err, -1.0 / method_order), params_.facmin)); - - next_step.accept = err <= 1; - next_step.step_size *= h_mult; - - return next_step; - } + StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) final; constexpr bool usesError() const final { @@ -1023,15 +254,9 @@ namespace Integrator class FixedStep : public StepController { - StepControl nextStep([[maybe_unused]] double err, [[maybe_unused]] StepControl prev_step, [[maybe_unused]] uint8_t method_order) - { - return StepControl{ - .accept = true, - .step_size = prev_step.step_size, - }; - } + StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) final; - bool usesError() const final + constexpr bool usesError() const final { return false; } @@ -1057,23 +282,6 @@ namespace Integrator { } - double errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const final - { - double one = 1.0; - workspace_.out_->copyFromExternal(&err, memspace, memspace); - workspace_.scale_->copyFromExternal(&y, memspace, memspace); - workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace); - - handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); - handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); - handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.yprev_abs_.get(), memspace); - handler.scal(params_.rtol, workspace_.scale_.get(), memspace); - handler.axpy(one, params_.atol.get(), workspace_.scale_.get(), memspace); - handler.scaleInv(workspace_.scale_.get(), workspace_.out_.get(), memspace); - - return handler.amax(workspace_.out_.get(), memspace); - } + double errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const final; }; } // namespace Integrator - -#undef BUBBLE_FAIL \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp new file mode 100644 index 000000000..33379be54 --- /dev/null +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -0,0 +1,190 @@ +#include + +#include "Rosenbrock.hpp" + +namespace Integrator +{ + template + Rosenbrock::Tableau Rosenbrock::Tableau::lin_implicit_euler() + { + constexpr size_t num_stages = 1; + + Tableau re = { + .num_stages = num_stages, + .gamma = 1.0, + .alpha_sum = std::make_unique_for_overwrite(num_stages), + .gamma_sum = std::make_unique_for_overwrite(num_stages), + .m = std::make_unique_for_overwrite(num_stages), + .e = {}, + .A = std::make_unique_for_overwrite(num_stages * num_stages), + .C = std::make_unique_for_overwrite(num_stages * num_stages), + .H = {}, + .order = 1, + .is_krylov = true, + .is_w = false, + .is_dae = true, + }; + + re.alpha_sum[0] = 0.0; + + re.gamma_sum[0] = 1.0; + + re.m[0] = 1.0; + + re.A[0] = 0.0; + + re.C[0] = 2.0; + + return re; + } + + template + Rosenbrock::Tableau Rosenbrock::Tableau::rodas5p() + { + constexpr size_t num_stages = 8; + + Tableau re = { + .num_stages = num_stages, + .gamma = 0.21193756319429014, + .alpha_sum = std::make_unique_for_overwrite(num_stages), + .gamma_sum = std::make_unique_for_overwrite(num_stages), + .m = std::make_unique_for_overwrite(num_stages), + .e = {}, + .A = std::make_unique_for_overwrite(num_stages * num_stages), + .C = std::make_unique_for_overwrite(num_stages * num_stages), + .H = std::make_unique_for_overwrite(num_stages * 3), + .order = 5, + .is_krylov = false, + .is_w = false, + .is_dae = true, + }; + + re.alpha_sum[0] = 0.0; + re.alpha_sum[1] = 0.6358126895828704; + re.alpha_sum[2] = 0.4095798393397535; + re.alpha_sum[3] = 0.9769306725060716; + re.alpha_sum[4] = 0.4288403609558664; + re.alpha_sum[5] = 1.0; + re.alpha_sum[6] = 1.0; + re.alpha_sum[7] = 1.0; + + re.gamma_sum[0] = 0.21193756319429014; + re.gamma_sum[1] = -0.42387512638858027; + re.gamma_sum[2] = -0.3384627126235924; + re.gamma_sum[3] = 1.8046452872882734; + re.gamma_sum[4] = 2.325825639765069; + re.gamma_sum[5] = 0.0; + re.gamma_sum[6] = 0.0; + re.gamma_sum[7] = 0.0; + + re.m[0] = -7.502846399306121; + re.m[1] = 2.561846144803919; + re.m[2] = -11.627539656261098; + re.m[3] = -0.18268767659942256; + re.m[4] = 0.030198172008377946; + re.m[5] = 1.0; + re.m[6] = 1.0; + re.m[7] = 1.0; + + re.A[1 * 8 + 0] = 3.0; + + re.A[2 * 8 + 0] = 2.849394379747939; + re.A[2 * 8 + 1] = 0.45842242204463923; + + re.A[3 * 8 + 0] = -6.954028509809101; + re.A[3 * 8 + 1] = 2.489845061869568; + re.A[3 * 8 + 2] = -10.358996098473584; + + re.A[4 * 8 + 0] = 2.8029986275628964; + re.A[4 * 8 + 1] = 0.5072464736228206; + re.A[4 * 8 + 2] = -0.3988312541770524; + re.A[4 * 8 + 3] = -0.04721187230404641; + + re.A[5 * 8 + 0] = -7.502846399306121; + re.A[5 * 8 + 1] = 2.561846144803919; + re.A[5 * 8 + 2] = -11.627539656261098; + re.A[5 * 8 + 3] = -0.18268767659942256; + re.A[5 * 8 + 4] = 0.030198172008377946; + + re.A[6 * 8 + 0] = -7.502846399306121; + re.A[6 * 8 + 1] = 2.561846144803919; + re.A[6 * 8 + 2] = -11.627539656261098; + re.A[6 * 8 + 3] = -0.18268767659942256; + re.A[6 * 8 + 4] = 0.030198172008377946; + re.A[6 * 8 + 5] = 1.0; + + re.A[7 * 8 + 0] = -7.502846399306121; + re.A[7 * 8 + 1] = 2.561846144803919; + re.A[7 * 8 + 2] = -11.627539656261098; + re.A[7 * 8 + 3] = -0.18268767659942256; + re.A[7 * 8 + 4] = 0.030198172008377946; + re.A[7 * 8 + 5] = 1.0; + re.A[7 * 8 + 6] = 1.0; + + re.C[1 * 8 + 0] = -14.155112264123755; + + re.C[2 * 8 + 0] = -17.97296035885952; + re.C[2 * 8 + 1] = -2.859693295451294; + + re.C[3 * 8 + 0] = 147.12150275711716; + re.C[3 * 8 + 1] = -1.41221402718213; + re.C[3 * 8 + 2] = 71.68940251302358; + + re.C[4 * 8 + 0] = 165.43517024871676; + re.C[4 * 8 + 1] = -0.4592823456491126; + re.C[4 * 8 + 2] = 42.90938336958603; + re.C[4 * 8 + 3] = -5.961986721573306; + + re.C[5 * 8 + 0] = 24.854864614690072; + re.C[5 * 8 + 1] = -3.0009227002832186; + re.C[5 * 8 + 2] = 47.4931110020768; + re.C[5 * 8 + 3] = 5.5814197821558125; + re.C[5 * 8 + 4] = -0.6610691825249471; + + re.C[6 * 8 + 0] = 30.91273214028599; + re.C[6 * 8 + 1] = -3.1208243349937974; + re.C[6 * 8 + 2] = 77.79954646070892; + re.C[6 * 8 + 3] = 34.28646028294783; + re.C[6 * 8 + 4] = -19.097331116725623; + re.C[6 * 8 + 5] = -28.087943162872662; + + re.C[7 * 8 + 0] = 37.80277123390563; + re.C[7 * 8 + 1] = -3.2571969029072276; + re.C[7 * 8 + 2] = 112.26918849496327; + re.C[7 * 8 + 3] = 66.9347231244047; + re.C[7 * 8 + 4] = -40.06618937091002; + re.C[7 * 8 + 5] = -54.66780262877968; + re.C[7 * 8 + 6] = -9.48861652309627; + + re.H[0 * 8 + 0] = 25.948786856663858; + re.H[0 * 8 + 1] = -2.5579724845846235; + re.H[0 * 8 + 2] = 10.433815404888879; + re.H[0 * 8 + 3] = -2.3679251022685204; + re.H[0 * 8 + 4] = 0.524948541321073; + re.H[0 * 8 + 5] = 1.1241088310450404; + re.H[0 * 8 + 6] = 0.4272876194431874; + re.H[0 * 8 + 7] = -0.17202221070155493; + + re.H[1 * 8 + 0] = -9.91568850695171; + re.H[1 * 8 + 1] = -0.9689944594115154; + re.H[1 * 8 + 2] = 3.0438037242978453; + re.H[1 * 8 + 3] = -24.495224566215796; + re.H[1 * 8 + 4] = 20.176138334709044; + re.H[1 * 8 + 5] = 15.98066361424651; + re.H[1 * 8 + 6] = -6.789040303419874; + re.H[1 * 8 + 7] = -6.710236069923372; + + re.H[2 * 8 + 0] = 11.419903575922262; + re.H[2 * 8 + 1] = 2.8879645146136994; + re.H[2 * 8 + 2] = 72.92137995996029; + re.H[2 * 8 + 3] = 80.12511834622643; + re.H[2 * 8 + 4] = -52.072871366152654; + re.H[2 * 8 + 5] = -59.78993625266729; + re.H[2 * 8 + 6] = -0.15582684282751913; + re.H[2 * 8 + 7] = 4.883087185713722; + + return re; + } + + template class Rosenbrock; +} // namespace Integrator \ No newline at end of file From 480da9706c7193eee130bee35e6d26de278c65d5 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 17 Mar 2026 15:23:26 -0400 Subject: [PATCH 14/57] Add embedded coefficients for rodas5p --- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp index 33379be54..9ed39622d 100644 --- a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -49,7 +49,7 @@ namespace Integrator .alpha_sum = std::make_unique_for_overwrite(num_stages), .gamma_sum = std::make_unique_for_overwrite(num_stages), .m = std::make_unique_for_overwrite(num_stages), - .e = {}, + .e = std::make_unique_for_overwrite(num_stages), .A = std::make_unique_for_overwrite(num_stages * num_stages), .C = std::make_unique_for_overwrite(num_stages * num_stages), .H = std::make_unique_for_overwrite(num_stages * 3), @@ -86,6 +86,15 @@ namespace Integrator re.m[6] = 1.0; re.m[7] = 1.0; + re.e[0] = 0.0; + re.e[1] = 0.0; + re.e[2] = 0.0; + re.e[3] = 0.0; + re.e[4] = 0.0; + re.e[5] = 0.0; + re.e[6] = 0.0; + re.e[7] = 1.0; + re.A[1 * 8 + 0] = 3.0; re.A[2 * 8 + 0] = 2.849394379747939; From 227780eb4022cb5c3228dc61811e894acd104e87 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 17 Mar 2026 15:23:47 -0400 Subject: [PATCH 15/57] Documentation and organization --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 111 ++--- GridKit/Solver/Dynamic/Rosenbrock.hpp | 578 +++++++++++++++++++++++--- 2 files changed, 573 insertions(+), 116 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 69ab5e25a..85e71f3ff 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -160,43 +160,43 @@ namespace Integrator { size_t size = static_cast(model_->size()); - y_prev_ = std::make_unique(size); - y_cur_ = std::make_unique(size); - y_interp_ = std::make_unique(size); - asum_ = std::make_unique(size); - csum_ = std::make_unique(size); - RHS_ = std::make_unique(size); - RHS_first_stage_ = std::make_unique(size); - dFdt_ = std::make_unique(size); - mass_ = std::make_unique(size); - y_new_ = std::make_unique(size); + y_prev_ = std::make_unique(size); + y_cur_ = std::make_unique(size); + y_new_ = std::make_unique(size); + y_interp_ = std::make_unique(size); + workspace_.asum_ = std::make_unique(size); + workspace_.csum_ = std::make_unique(size); + workspace_.RHS_ = std::make_unique(size); + workspace_.RHS_first_stage_ = std::make_unique(size); + workspace_.dFdt_ = std::make_unique(size); + workspace_.mass_ = std::make_unique(size); BUBBLE_FAIL(y_prev_->allocate(memspace_)); BUBBLE_FAIL(y_cur_->allocate(memspace_)); - BUBBLE_FAIL(y_interp_->allocate(memspace_)); - BUBBLE_FAIL(asum_->allocate(memspace_)); - BUBBLE_FAIL(csum_->allocate(memspace_)); - BUBBLE_FAIL(RHS_->allocate(memspace_)); - BUBBLE_FAIL(RHS_first_stage_->allocate(memspace_)); - BUBBLE_FAIL(dFdt_->allocate(memspace_)); - BUBBLE_FAIL(mass_->allocate(memspace_)); BUBBLE_FAIL(y_new_->allocate(memspace_)); + BUBBLE_FAIL(y_interp_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.asum_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.csum_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.RHS_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.RHS_first_stage_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.dFdt_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.mass_->allocate(memspace_)); if (tab_.e) { auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); if (!can_use_stage) { - err_est_ = std::make_unique(size); - BUBBLE_FAIL(err_est_->allocate(memspace_)); + workspace_.err_est_ = std::make_unique(size); + BUBBLE_FAIL(workspace_.err_est_->allocate(memspace_)); } } - stages_ = std::make_unique[]>(tab_.num_stages); + workspace_.stages_ = std::make_unique[]>(tab_.num_stages); for (size_t i = 0; i < tab_.num_stages; i++) { - stages_[i] = std::make_unique(size); - BUBBLE_FAIL(stages_[i]->allocate(memspace_)); + workspace_.stages_[i] = std::make_unique(size); + BUBBLE_FAIL(workspace_.stages_[i]->allocate(memspace_)); } if (tab_.order > 2) @@ -209,7 +209,7 @@ namespace Integrator } } - jacobian_ = std::make_unique(size, size, model_->getCsrJacobian()->getNnz()); + workspace_.jacobian_ = std::make_unique(size, size, model_->getCsrJacobian()->getNnz()); return 0; } @@ -219,15 +219,15 @@ namespace Integrator { current_time_ = t0; BUBBLE_FAIL(y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_)); - jacobian_analyzed_ = false; + workspace_.jacobian_analyzed_ = false; GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); - BUBBLE_FAIL(jacobian_->setDataPointers( + BUBBLE_FAIL(workspace_.jacobian_->setDataPointers( model_jacobian->getRowData(), model_jacobian->getColData(), model_jacobian->getValues(), memspace_)); - BUBBLE_FAIL(lin_solver_.setMatrix(jacobian_.get())); + BUBBLE_FAIL(lin_solver_.setMatrix(workspace_.jacobian_.get())); BUBBLE_FAIL(lin_solver_.analyze()); BUBBLE_FAIL(lin_solver_.preconditionerSetup()); @@ -242,7 +242,7 @@ namespace Integrator { mass[i] = model_->tag()[i] ? 1.0 : 0.0; } - BUBBLE_FAIL(mass_->copyFromExternal(mass.get(), memspace_, memspace_)); + BUBBLE_FAIL(workspace_.mass_->copyFromExternal(mass.get(), memspace_, memspace_)); stats_ = Stats(); @@ -331,7 +331,7 @@ namespace Integrator std::swap(y_prev_, y_cur_); std::swap(y_cur_, y_new_); - dense_coefficients_valid = false; + dense_coefficients_valid_ = false; } else { @@ -383,10 +383,10 @@ namespace Integrator { if (tab_.hasDenseOutput()) { - if (!dense_coefficients_valid) + if (!dense_coefficients_valid_) { BUBBLE_FAIL(calc_dense_coeff()); - dense_coefficients_valid = true; + dense_coefficients_valid_ = true; } double theta = (out_time - current_time_) / prev_step_size_ + 1; @@ -434,20 +434,21 @@ namespace Integrator model_->updateTime(t0, -1.0 / (dt * tab_.gamma)); BUBBLE_FAIL(model_->evaluateJacobian()); GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); - BUBBLE_FAIL(jacobian_->setDataPointers( + BUBBLE_FAIL(workspace_.jacobian_->setDataPointers( model_jacobian->getRowData(), model_jacobian->getColData(), model_jacobian->getValues(), memspace_)); [[likely]] - if (jacobian_analyzed_) + if (workspace_.jacobian_analyzed_) { BUBBLE_FAIL(lin_solver_.refactorize()); } else { BUBBLE_FAIL(lin_solver_.factorize()); + workspace_.jacobian_analyzed_ = true; } stats_.jac_evals++; @@ -469,12 +470,12 @@ namespace Integrator } model_->updateTime(t0, 0.0); BUBBLE_FAIL(model_->evaluateResidual()); - BUBBLE_FAIL(RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); - vector_handler_.scal(-1, RHS_first_stage_.get(), memspace_); + BUBBLE_FAIL(workspace_.RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); + vector_handler_.scal(-1, workspace_.RHS_first_stage_.get(), memspace_); stats_.f_evals++; } - BUBBLE_FAIL(lin_solver_.solve(RHS_first_stage_.get(), stages_[0].get())); + BUBBLE_FAIL(lin_solver_.solve(workspace_.RHS_first_stage_.get(), workspace_.stages_[0].get())); stats_.decomp_solves++; // Rest of stages @@ -485,41 +486,41 @@ namespace Integrator if (i > 1 && tab_.can_reuse_asum(i)) { if (tab_.A[tab_.num_stages * i + i - 1] != 0.0) - vector_handler_.axpy(tab_.A[tab_.num_stages * i + i - 1], stages_[i - 1].get(), asum_.get(), memspace_); + vector_handler_.axpy(tab_.A[tab_.num_stages * i + i - 1], workspace_.stages_[i - 1].get(), workspace_.asum_.get(), memspace_); } else { - BUBBLE_FAIL(asum_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); + BUBBLE_FAIL(workspace_.asum_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); for (size_t j = 0; j < i; j++) { if (tab_.A[tab_.num_stages * i + j] != 0.0) - vector_handler_.axpy(tab_.A[tab_.num_stages * i + j], stages_[j].get(), asum_.get(), memspace_); + vector_handler_.axpy(tab_.A[tab_.num_stages * i + j], workspace_.stages_[j].get(), workspace_.asum_.get(), memspace_); } } // Calculate csum // TODO: Since csum is multiplied by the mass matrix, we can reduce calculations by just not calculating some indices - BUBBLE_FAIL(csum_->setToZero(memspace_)); + BUBBLE_FAIL(workspace_.csum_->setToZero(memspace_)); for (size_t j = 0; j < i; j++) { if (tab_.C[i * tab_.num_stages + j] != 0.0) { - vector_handler_.axpy(tab_.C[i * tab_.num_stages + j] / dt, stages_[j].get(), csum_.get(), memspace_); + vector_handler_.axpy(tab_.C[i * tab_.num_stages + j] / dt, workspace_.stages_[j].get(), workspace_.csum_.get(), memspace_); } } // TODO: non-autonomous model - BUBBLE_FAIL(asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); + BUBBLE_FAIL(workspace_.asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); model_->updateTime(t0 + tab_.alpha_sum[i] * dt, 0.0); BUBBLE_FAIL(model_->evaluateResidual()); - RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); + workspace_.RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); - vector_handler_.scal(-1, RHS_.get(), memspace_); - vector_handler_.scal(mass_.get(), csum_.get(), memspace_); - vector_handler_.axpy(-1, csum_.get(), RHS_.get(), memspace_); + vector_handler_.scal(-1, workspace_.RHS_.get(), memspace_); + vector_handler_.scal(workspace_.mass_.get(), workspace_.csum_.get(), memspace_); + vector_handler_.axpy(-1, workspace_.csum_.get(), workspace_.RHS_.get(), memspace_); - BUBBLE_FAIL(lin_solver_.solve(RHS_.get(), stages_[i].get())); + BUBBLE_FAIL(lin_solver_.solve(workspace_.RHS_.get(), workspace_.stages_[i].get())); stats_.f_evals++; stats_.decomp_solves++; } @@ -529,8 +530,8 @@ namespace Integrator // plus some multiple of the last stage. In that case we can avoid a matmul if (tab_.can_reuse_asum_for_out()) { - std::swap(asum_, y_new_); - vector_handler_.axpy(tab_.m[tab_.num_stages - 1], stages_[tab_.num_stages - 1].get(), y_new_.get(), memspace_); + std::swap(workspace_.asum_, y_new_); + vector_handler_.axpy(tab_.m[tab_.num_stages - 1], workspace_.stages_[tab_.num_stages - 1].get(), y_new_.get(), memspace_); } else { @@ -540,7 +541,7 @@ namespace Integrator { if (tab_.m[j] != 0.0) { - vector_handler_.axpy(tab_.m[j], stages_[j].get(), y_new_.get(), memspace_); + vector_handler_.axpy(tab_.m[j], workspace_.stages_[j].get(), y_new_.get(), memspace_); } } } @@ -555,28 +556,28 @@ namespace Integrator if (can_use_stage) { - return *stages_[err_stage]; + return *workspace_.stages_[err_stage]; } else { // TODO: could make this function return recoverable errors by using std::variant - int err_code = err_est_->copyFromExternal(stages_[0].get(), memspace_, memspace_); + int err_code = workspace_.err_est_->copyFromExternal(workspace_.stages_[0].get(), memspace_, memspace_); if (err_code) { throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); } - vector_handler_.scal(tab_.e[0], stages_[0].get(), memspace_); + vector_handler_.scal(tab_.e[0], workspace_.stages_[0].get(), memspace_); for (size_t j = 1; j < tab_.num_stages; j++) { if (tab_.e[j] != 0.0) { - vector_handler_.axpy(tab_.e[j], stages_[j].get(), err_est_.get(), memspace_); + vector_handler_.axpy(tab_.e[j], workspace_.stages_[j].get(), workspace_.err_est_.get(), memspace_); } } - return *err_est_; + return *workspace_.err_est_; } } @@ -594,7 +595,7 @@ namespace Integrator { for (size_t j = 0; j < tab_.order - 2; j++) { - vector_handler_.axpy(tab_.H[j * tab_.num_stages + i], stages_[i].get(), dense_coeff_[j].get(), memspace_); + vector_handler_.axpy(tab_.H[j * tab_.num_stages + i], workspace_.stages_[i].get(), dense_coeff_[j].get(), memspace_); } } } diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 33b56ffd1..23588bc3d 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -22,121 +22,362 @@ namespace Integrator { using State = ReSolve::vector::Vector; + /** + * @brief Define control flow for `StepController`s to be able to control the step size of a `Rosenbrock` integrator. + * + */ struct StepControl { - bool accept; + /** + * @brief Whether or not the step is accepted. A rejected step will cause the time step controller to discard + * the next state and re-step with the new `step_size`. + * + */ + bool accept; + + /** + * @brief The step size the next step should take. + * + */ double step_size; }; + /** + * @brief Interface for step size controllers. Used by `Rosenbrock` integrators to decide when to accept/reject steps and + * what size each step should be. + * + */ class StepController { public: + /** + * @brief Decide the control flow for the next step, based on information gathered by the integrator about the current step. + * + * @param err The estimated error made by the current step. Only calculated if `usesError()` returns true, otherwise it is assumed this + * value is not used. + * @param prev_step The control flow from before the current step was taken. Can be used to accurately update the step size. + * @param method_order The order of the method being used. + * @return StepControl + */ virtual StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) = 0; - virtual bool usesError() const = 0; + + /** + * @brief Return whether or not the `nextStep` method implementation uses the `err` parameter. If `false`, this parameter is not calculated. + * + */ + virtual bool usesError() const = 0; }; + /** + * @brief Interface for error norms. Used to calculate the `err` parameter in `StepController::nextStep` based on a residual state error vector. + * + */ class ErrorNorm { public: + /** + * @brief Calculate an error to be used by a step controller. Typically, an error > 1 indicates an error which does not meet tolerances, while + * an error < 1 indicates an error which meets tolerances. For that reason, tolerances should be included in the calculation of the error. + * + * @param err The state error residual being measured. + * @param y The state that the error was calculated from. Can be used for proper relative error normalization. + * @param yprev The state from the previous step. Can be used for proper relative error normalization. + * @param handler A vector handler which can be used to facilitate vector operations. + * @param memspace The memory space which vector operations should be performed in/. + * @return double The error. + */ virtual double errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const = 0; }; + /** + * @brief A Rosenbrock integrator designed to simulate a `GridKit::Model::Evaluator` model. + * Rosenbrock integrators are best for quick medium-accuracy simulation and + * boast a large amount of customization to simulating a given model. + * + * For the list of available Rosenbrock methods, see `Rosenbrock::Tableau`. + */ template class Rosenbrock { using RealT = typename GridKit::ScalarTraits::RealT; public: + /** + * @brief Keeps track of a variety of notable properties of a single step. + * + */ struct StepInfo { + /** + * @brief The simulation time at the beginning of the step + * + */ double sim_time; + + /** + * @brief The size of the step. + * + */ double step_size; + + /** + * @brief The size of the next step, as governed by the current `StepController` in use. + * + */ double next_step_size; + + /** + * @brief The estimated error made by the step, as calculated by the current `ErrorNorm` in use. + * + */ double err_est; + + /** + * @brief The step number, starting at 1. + * + */ size_t step_no; - bool skip_lu; - bool skip_f; - bool accepted; + + /** + * @brief Whether or not the integrator decided to skip computing the decomposition of the Jacobian on this step. + * + */ + bool skip_lu; + + /** + * @brief Whether or not the integrator decided to skip evaluating the residual on the first stage on this step. + * + */ + bool skip_f; + + /** + * @brief Whether or not this step was accepted by the `StepController` in use. + * + */ + bool accepted; std::string csv_report() const; std::string report() const; }; + /** + * @brief Keeps track of running statistics of the integrator since the last `initializeSimulation()` call. + * + */ struct Stats { + /** + * @brief Information of each step which has been rejected. + * + */ std::vector rejections; + + /** + * @brief Information of each step which the integrator decided to skip re-factoring the Jacobian. + * + */ std::vector skip_lu_steps; - size_t num_steps = 0; - size_t f_evals = 0; - size_t f_skipped = 0; - size_t jac_evals = 0; - size_t decomp_solves = 0; - double min_step = INFINITY; - double max_step = 0; + + /** + * @brief How many steps the integrator has taken. + * + */ + size_t num_steps = 0; + + /** + * @brief Number of model residual function evaluations. + * + */ + size_t f_evals = 0; + + /** + * @brief Number of model residual function evaluations which have been skipped by the integrator. + * + */ + size_t f_skipped = 0; + + /** + * @brief Number of model Jacobian evaluations. + * + */ + size_t jac_evals = 0; + + /** + * @brief Number of linear solves against the model Jacobian. + * + */ + size_t decomp_solves = 0; + + /** + * @brief Minimum step size. + * + */ + double min_step = INFINITY; + + /** + * @brief Maximum step size. + * + */ + double max_step = 0; std::string report() const; Stats& operator+=(const Stats& other); }; + /** + * @brief Parameters of the integrator. + * + */ struct Parameters { - double atol = 1e-5; - double rtol = 1e-5; + /** + * @brief What step size the first step should take. + * + * @todo Consider adding a starting step size selector to select this automatically. + */ double starting_step = 1e-5; - size_t max_steps = 2000; - bool skip_lu = false; + + /** + * @brief The maximum number of steps the integrator should take. If the integrator has not reached the final time before + * taking this many steps, then integration is stopped. For more details, see `integrate()`. + * + */ + size_t max_steps = 2000; + + /** + * @brief Whether or not the integrator should attempt to skip Jacobian decompositions. + * + * @note This feature is only available if the underlying method is a Rosenbrock-W method and can speed up + * the time taken to compute each step. However, the overall number of steps taken will increase. + * + */ + bool skip_lu = false; }; + /** + * @brief A list of the coefficients needed to complete Rosenbrock integration in an accurate way. + * Each tableau can be considered a different method or scheme, and will have different properties, + * advantages, and disadvantages. + * + */ struct Tableau { - + /** + * @brief The number of stages used by the method. Each stage requires one model residual evaluation. + * + */ size_t num_stages; - /// @brief The coefficient along the diagonal of the Gamma matrix. + /** + * @brief The coefficient along the diagonal of the Gamma matrix. + * + */ RealT gamma; - /// @brief A vector of sums of rows of the alpha matrix. These are the classic - /// Runge-Kutta 'c' coefficients, or abscissae. + /** + * @brief A vector of sums of rows of the alpha matrix. These are the classic + * Runge-Kutta 'c' coefficients, or abscissae. The size of this vector + * should be equal to `num_stages`. + * + */ std::unique_ptr alpha_sum; - /// @brief A vector of sums of rows of the Gamma matrix. + + /** + * @brief A vector of sums of rows of the Gamma matrix. The size of this vector + * should be equal to `num_stages`. + * + */ std::unique_ptr gamma_sum; - /// @brief A vector of weights for constructing the final solution from the stages + + /** + * @brief A vector of weights for constructing the final solution from the stages. + * The size of this vector should be equal to `num_stages`. + * + */ std::unique_ptr m; - /// @brief Coefficients for the embedded method. If `HAS_EMBEDDED == false`, then - /// this is nothing. + /** + * @brief OPTIONAL vector of coefficients for the embedded error method. If it exists, + * the size of this vector should be equal to `num_stages`. + * + */ std::unique_ptr e; - /// @brief The transformed A coefficient matrix. Strictly lower triangular. + /** + * @brief The transformed A coefficient matrix. Strictly lower triangular and stored in dense row-major form. + * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. + * + */ std::unique_ptr A; - /// @brief The transformed C coefficient matrix. Strictly lower triangular. + + /** + * @brief The transformed C coefficient matrix. Strictly lower triangular and stored in dense row-major form. + * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. + * + */ std::unique_ptr C; - /// @brief The matrix of dense coefficients. + /** + * @brief OPTIONAL matrix of dense coefficients. Defines how the stages should be transformed into interpolant + * nodes for computing dense output. The interpolating polynomial has an order one less than the order of + * the method, and two interpolant nodes are already pre-computed, so if this matrix exists it should be + * `order` - 2 by `num_stages` large. + * + */ std::unique_ptr H; - /// @brief What ODE order these coefficients satisfy. + /** + * @brief What ODE order these coefficients satisfy. If `is_dae` is true, then the coefficients must additionally satisfy + * DAE conditions up to this order. If `is_w` is true, then the coefficients must additionally satisfy ROW conditions + * up to this order. If `is_krylov` is true, then the coefficients must additionally satisfy ROK condition up to this order. + * + */ + uint8_t order; - /// @brief Whether or not these coefficients satisfy Rosenbrock-Krylov (ROK) order conditions up to - /// `order`. + /** + * @brief Whether or not these coefficients are appropriate to use in a Rosenbrock-Krylov (ROK) solver. + * + */ bool is_krylov; - /// @brief Whether or not these coefficients satisfy Rosenbrock-W (ROW) order conditions up to `order`. + + /** + * @brief Whether or not these coefficients satisfy Rosenbrock-W (ROW) order conditions up to `order`. + * The integrator may take advantage of this fact by e.g. using time-delay Jacobians to speed up computation. + * + */ bool is_w; - /// @brief Whether or not these coefficients satisfy DAE order conditions up to `order`. + + /** + * @brief Whether or not these coefficients satisfy DAE order conditions up to `order`. If this is not true, + * these coefficients should not be used to solve models with algebraic conditions (indicated by a + * `Model::Evaluator::tag_` value of 0). + * + */ bool is_dae; - /// @brief Whether or not this tableau contains an embedded method + /** + * @brief Whether or not this tableau contains an embedded error estimator method. + * + */ constexpr bool has_embedded() const { return static_cast(e); } + /** + * @brief Whether or not this tableau contains coefficients which can be used to construct dense output. + * + */ constexpr bool hasDenseOutput() const { return static_cast(H); } + /** + * @brief Helper function for accessing elements of `A` + * + */ constexpr RealT getA(size_t row, size_t col) const { return A[row * num_stages + col]; @@ -151,34 +392,124 @@ namespace Integrator }; private: - double step_size_ = 0; + /** + * @brief The current step size of the integrator. If integration is ever stopped and resumed, this value will be used for + * the initial step after resuming. + * + */ + double step_size_ = 0; + + /** + * @brief The step size of the previous step. Used for operations which need to be done on the current step, but step size + * control for the next step has already been performed. + * + */ double prev_step_size_ = 0; + /** + * @brief Whether or not the integrator should attempt to skip Jacobian decomposition on the next step. Controlled by the + * time stepping algorithm in `integrate()`. Generally, this should only be set if we suspect the Jacobian for the + * last step is a good enough approximation of the Jacobian on the next step. Non-ROW methods need exact Jacobians, + * so this should only be set for ROW methods, and when the step size for the next step is the same as the step size + * as the previous step. + * + */ bool skip_lu_ = false; - bool skip_f_ = false; - - /// @brief Keeps track of whether or not the integrator currently has valid dense coefficients. - /// i.e. they have been computed and haven't been invalidated by taking another step. - bool dense_coefficients_valid = false; + /** + * @brief Whether or not the integrator should attempt to skip the residual function evaluation of the first stage on the + * next step. This should only be used when a step is rejected and the residual function is evaluated at the exact + * same arguments as the previous step. Then `RHS_first_stage_` can be re-used rather than re-calculated. + * + */ + bool skip_f_ = false; + + /** + * @brief Keeps track of whether or not the integrator currently has valid dense coefficients. + * i.e. they have been computed and haven't been invalidated by taking another step. This can be used to avoid + * re-computing dense coefficients when interpolating states multiple times in one step. + * + */ + bool dense_coefficients_valid_ = false; + + /** + * @brief The tableau of Rosenbrock coefficients currently being used by the integrator. + * + */ Tableau tab_; + /** + * @brief The model being simulated. + * + */ GridKit::Model::Evaluator* model_; - ReSolve::SystemSolver& lin_solver_; - ReSolve::VectorHandler& vector_handler_; - const ErrorNorm* err_norm_; - ReSolve::memory::MemorySpace memspace_; - double current_time_; + /** + * @brief The linear solver to be used during integration in `time_step()`. + * + */ + ReSolve::SystemSolver& lin_solver_; + /** + * @brief The vector handler to be used for vector operations by the integrator. + * + */ + ReSolve::VectorHandler& vector_handler_; + + /** + * @brief The `ErrorNorm` to be used by the `StepController` in `integrate()`. + * + * @note Can be `nullptr` if no `ErrorNorm` is configured, in case the `StepController` does not need an error to be calculated. + * + */ + const ErrorNorm* err_norm_; + + /** + * @brief The memory space where linear algebra operations hsould be done in. + * + */ + ReSolve::memory::MemorySpace memspace_; + + /** + * @brief The current simulation time. + * + */ + double current_time_; + + /** + * @brief The state from last step. + * + */ std::unique_ptr y_prev_; + + /** + * @brief The state on the current step. + * + */ std::unique_ptr y_cur_; + + /** + * @brief The incoming state for the next step. + * + */ + std::unique_ptr y_new_; + + /** + * @brief Used as output for dense output interpolation. + * + */ std::unique_ptr y_interp_; + /** + * @brief Configured parameters for the integrator. + * + */ Parameters params_; - Stats stats_; - Rosenbrock(const Rosenbrock& other) = delete; - Rosenbrock(Rosenbrock&& other) = delete; + /** + * @brief Running statistics of the integrator. Reset by `initializeSimulation()`. + * + */ + Stats stats_; public: Rosenbrock(Tableau&& tab, @@ -202,19 +533,90 @@ namespace Integrator std::optional> step_cb = {}); private: - std::unique_ptr asum_; - std::unique_ptr csum_; - std::unique_ptr RHS_; - std::unique_ptr RHS_first_stage_; - std::unique_ptr dFdt_; - std::unique_ptr mass_; - std::unique_ptr y_new_; - std::unique_ptr err_est_; - - std::unique_ptr jacobian_; - bool jacobian_analyzed_ = false; + /** + * @brief A workspace used by `time_step` for the necessary linear algebra computations. + * + */ + mutable struct + { + /** + * @brief Sum of A-weighted states used for function evaluation in a stage. + * + */ + std::unique_ptr asum_; + + /** + * @brief Sum of C-weighted states used in linearization in a stage. + * + */ + std::unique_ptr csum_; + + /** + * @brief Right-hand side of linear solve used in a stage. + * + */ + std::unique_ptr RHS_; + + /** + * @brief Right-hand side of linear solve used in the first stage. + * Stored separately from `RHS_` since it can be re-used by future steps if the current step is rejected. + * + * @see `skip_f_` + * + */ + std::unique_ptr RHS_first_stage_; + + /** + * @brief Time Jacobian for non-autonomous models. Currently unused, since models are assumed to be autonomous. + * + */ + std::unique_ptr dFdt_; + + /** + * @brief Vector representation of a diagonal mass matrix. + * + */ + std::unique_ptr mass_; + + /** + * @brief Estimated error produced by a step in a method with an empbedded error estimator. + * + * @see `Tableau::e` + * + */ + std::unique_ptr err_est_; + + /** + * @brief Jacobian matrix + * + */ + std::unique_ptr jacobian_; + + /** + * @brief Whether or not the Jacobian has been factorized. Initial factorization is required, + * but pivots can be re-used for a refactorization later. Re-factorization can introduce + * errors when the Jacobian differs significantly, so this flag should be reset when + * a significant event can happen, such as re-initializing the simulation. + * + * @see `initializeSimulation()` + * + */ + bool jacobian_analyzed_ = false; + + /** + * @brief Integration stages - each a low-order approximation of the state at the abscissae given by + * the tableau. + * + */ + std::unique_ptr[]> stages_; + } workspace_; - std::unique_ptr[]> stages_; + /** + * @brief Dense output interpolation nodes. Used to generate output states in-between steps. + * + * @see `calc_dense_coeff()`, `interp_dense()` + * + */ std::unique_ptr[]> dense_coeff_; public: @@ -230,12 +632,46 @@ namespace Integrator int interp_dense(double theta); }; + /** + * @brief A simple textbook adaptive `StepController` which seeks to meet a relative and absolute tolerance + * based on an error estimate. + * + */ class AdaptiveStep : public StepController { + /** + * @brief Parameters for the step controller. + * + */ struct Parameters { - double facmin = 0.2; - double facmax = 5.0; + /** + * @brief The minimum multiple by which the step size can be multiplied to obtain the new step size. + * Increasing this can allow the integrator to be slightly more conservative in selecting the step size + * - decreasing the number of steps taken but increasing the risk of failing the next step. + * + * @note Should be between 0 and 1. + * + */ + double facmin = 0.2; + + /** + * @brief The maximum multiple by which the step size can be multiplied to obtain the new step size. + * Decreasing this will make the integrator more conservative in selecting the step size - + * increasing the number of steps taken but decreasing the risk of failing the next step. + * + * @note Should be greater than 1. + * + */ + double facmax = 5.0; + + /** + * @brief A "fudge factor" introduced to decrease risk of failing a step. The larger the fudge factor, + * the more likely steps will fail, but fewer steps will be taken. + * + * @note Should be between 0 and 1. + * + */ double facscale = 0.9; } params_; @@ -246,16 +682,36 @@ namespace Integrator StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) final; + /** + * @brief This controller uses error estimates. + * + * @see `nextStep()` + * + */ constexpr bool usesError() const final { return true; } }; + /** + * @brief A fixed step controller which doesn't change the step size and accepts every step. + * Useful if you know what time scale your simulation operates on apriori and you're + * using a method without an embedded error controller. + * + * To set the fixed size, set the `Rosenbrock::Parameters::starting_step` parameter. + * + */ class FixedStep : public StepController { StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) final; + /** + * @brief This controller does not use error estimates. + * + * @see `nextStep()` + * + */ constexpr bool usesError() const final { return false; From c5be4bc4e5ee70f42da3fd2ce76ec5e83b51ebdf Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 17 Mar 2026 15:50:37 -0400 Subject: [PATCH 16/57] Fix cmake when resolve not enabled --- tests/UnitTests/Solver/CMakeLists.txt | 4 +--- tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 18 +++++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/UnitTests/Solver/CMakeLists.txt b/tests/UnitTests/Solver/CMakeLists.txt index b6a9ff80a..5acde4132 100644 --- a/tests/UnitTests/Solver/CMakeLists.txt +++ b/tests/UnitTests/Solver/CMakeLists.txt @@ -3,6 +3,4 @@ # - Slaven Peles #]] -if(TARGET SUNDIALS::idas) - add_subdirectory(Dynamic) -endif() +add_subdirectory(Dynamic) diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index 4029f83bb..c9467d971 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -1,14 +1,18 @@ # -add_executable(test_ida runIdaTests.cpp) -target_link_libraries( - test_ida GridKit::solvers_dyn GridKit::testing) +if(TARGET SUNDIALS::idas) + add_executable(test_ida runIdaTests.cpp) + target_link_libraries( + test_ida GridKit::solvers_dyn GridKit::testing) -add_test(NAME IDATest COMMAND $) + add_test(NAME IDATest COMMAND $) +endif() -add_executable(test_ros runRosenbrockTests.cpp) -target_link_libraries(test_ros GridKit::rosenbrock GridKit::testing) -add_test(NAME ROSTest COMMAND $) +if (GRIDKIT_ENABLE_RESOLVE) + add_executable(test_ros runRosenbrockTests.cpp) + target_link_libraries(test_ros GridKit::rosenbrock GridKit::testing) + add_test(NAME ROSTest COMMAND $) +endif() install(TARGETS test_ida RUNTIME DESTINATION bin) From 8c5ea6644cee1c5e57138088ef7b3da00fe5035d Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 17 Mar 2026 16:27:24 -0400 Subject: [PATCH 17/57] Fix some documentation mistakes --- GridKit/Solver/Dynamic/Rosenbrock.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 23588bc3d..315bbbb2b 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -408,7 +408,7 @@ namespace Integrator /** * @brief Whether or not the integrator should attempt to skip Jacobian decomposition on the next step. Controlled by the - * time stepping algorithm in `integrate()`. Generally, this should only be set if we suspect the Jacobian for the + * time stepping algorithm in \link integrate() \endlink . Generally, this should only be set if we suspect the Jacobian for the * last step is a good enough approximation of the Jacobian on the next step. Non-ROW methods need exact Jacobians, * so this should only be set for ROW methods, and when the step size for the next step is the same as the step size * as the previous step. @@ -419,7 +419,7 @@ namespace Integrator /** * @brief Whether or not the integrator should attempt to skip the residual function evaluation of the first stage on the * next step. This should only be used when a step is rejected and the residual function is evaluated at the exact - * same arguments as the previous step. Then `RHS_first_stage_` can be re-used rather than re-calculated. + * same arguments as the previous step. Then \ref RHS_first_stage_ can be re-used rather than re-calculated. * */ bool skip_f_ = false; @@ -445,7 +445,7 @@ namespace Integrator GridKit::Model::Evaluator* model_; /** - * @brief The linear solver to be used during integration in `time_step()`. + * @brief The linear solver to be used during integration in \link time_step() \endlink. * */ ReSolve::SystemSolver& lin_solver_; @@ -456,7 +456,7 @@ namespace Integrator ReSolve::VectorHandler& vector_handler_; /** - * @brief The `ErrorNorm` to be used by the `StepController` in `integrate()`. + * @brief The `ErrorNorm` to be used by the `StepController` in \link integrate() \endlink. * * @note Can be `nullptr` if no `ErrorNorm` is configured, in case the `StepController` does not need an error to be calculated. * @@ -506,7 +506,7 @@ namespace Integrator Parameters params_; /** - * @brief Running statistics of the integrator. Reset by `initializeSimulation()`. + * @brief Running statistics of the integrator. Reset by \link initializeSimulation() \endlink. * */ Stats stats_; @@ -559,7 +559,7 @@ namespace Integrator /** * @brief Right-hand side of linear solve used in the first stage. - * Stored separately from `RHS_` since it can be re-used by future steps if the current step is rejected. + * Stored separately from \ref RHS_ since it can be re-used by future steps if the current step is rejected. * * @see `skip_f_` * @@ -598,7 +598,7 @@ namespace Integrator * errors when the Jacobian differs significantly, so this flag should be reset when * a significant event can happen, such as re-initializing the simulation. * - * @see `initializeSimulation()` + * @see \link initializeSimulation() \endlink * */ bool jacobian_analyzed_ = false; @@ -614,7 +614,7 @@ namespace Integrator /** * @brief Dense output interpolation nodes. Used to generate output states in-between steps. * - * @see `calc_dense_coeff()`, `interp_dense()` + * @see \link calc_dense_coeff() \endlink, \link interp_dense() \endlink * */ std::unique_ptr[]> dense_coeff_; From 33281cb954dac684ba0f16277ae9fa40b2effa93 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Wed, 18 Mar 2026 13:45:40 -0400 Subject: [PATCH 18/57] Make error_estimator_stage more ergonomic --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 20 ++++++++++---------- GridKit/Solver/Dynamic/Rosenbrock.hpp | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 85e71f3ff..b5e1385f3 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -122,18 +122,18 @@ namespace Integrator } template - constexpr std::tuple Rosenbrock::Tableau::error_estimator_stage() const + constexpr std::optional Rosenbrock::Tableau::error_estimator_stage() const { - std::tuple re = {false, 0}; + std::optional re; for (size_t j = 0; j < num_stages; j++) { - if (e[j] == 1.0 && !std::get<0>(re)) + if (e[j] == 1.0 && !re) { - re = {true, j}; + re = j; } else if (e[j] != 0.0) { - return {false, 0}; + return {}; } } return re; @@ -184,8 +184,8 @@ namespace Integrator if (tab_.e) { - auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); - if (!can_use_stage) + std::optional err_est_stage = tab_.error_estimator_stage(); + if (!err_est_stage) { workspace_.err_est_ = std::make_unique(size); BUBBLE_FAIL(workspace_.err_est_->allocate(memspace_)); @@ -552,11 +552,11 @@ namespace Integrator template State& Rosenbrock::error_estimate() const { - auto [can_use_stage, err_stage] = tab_.error_estimator_stage(); + std::optional err_stage = tab_.error_estimator_stage(); - if (can_use_stage) + if (err_stage) { - return *workspace_.stages_[err_stage]; + return *workspace_.stages_[*err_stage]; } else { diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 315bbbb2b..39fce01ca 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -383,9 +383,9 @@ namespace Integrator return A[row * num_stages + col]; } - constexpr bool can_reuse_asum(size_t stage) const; - constexpr bool can_reuse_asum_for_out() const; - constexpr std::tuple error_estimator_stage() const; + constexpr bool can_reuse_asum(size_t stage) const; + constexpr bool can_reuse_asum_for_out() const; + constexpr std::optional error_estimator_stage() const; static Tableau lin_implicit_euler(); static Tableau rodas5p(); From 4b69ae911859c3f1ce7a9b20954fcbfcacaac7b7 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 20 Mar 2026 16:19:46 -0400 Subject: [PATCH 19/57] Move documentation --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 183 +++++++++++++++++++++++++- GridKit/Solver/Dynamic/Rosenbrock.hpp | 151 ++++++++++----------- docs/CMakeLists.txt | 1 + 3 files changed, 251 insertions(+), 84 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index b5e1385f3..c4f9ce3ef 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -6,6 +6,14 @@ #include +/** + * @brief A small helper macro to "bubble" errors. The Rosenbrock implementations call many + * fallible model and linear algebra functions, and often the only thing that can be + * done when one of these fails is to fail the current function as well. This macro + * wraps a fallible function call and causes the current function to fail as well, + * returning the same error code as the called function. + * + */ #define BUBBLE_FAIL(arg) \ do \ { \ @@ -15,6 +23,12 @@ namespace Integrator { + /** + * @brief Constructs a single-line report string out of the `StepInfo` object which is suitable to be included in a CSV file. + * + * Useful if you would like to plot step info. + * + */ template std::string Rosenbrock::StepInfo::csv_report() const { @@ -32,6 +46,12 @@ namespace Integrator return out.str(); } + /** + * @brief Constructs a human-readable report string out of the `StepInfo` object. + * + * Useful for debug dumps. + * + */ template std::string Rosenbrock::StepInfo::report() const { @@ -49,6 +69,12 @@ namespace Integrator return out.str(); } + /** + * @brief Constructs a human-readable report string out of the `Stats` object. + * + * Useful for reporting at the end of a simulation. + * + */ template std::string Rosenbrock::Stats::report() const { @@ -66,6 +92,14 @@ namespace Integrator return out.str(); } + /** + * @brief Accumulate statistics into one object. Useful to collect statistics over runs of several simulations, such as when + * there is a discrete event. + * + * @param other The other statistics to add to this one. + * + * @todo Right now, the step numbers for \ref rejections and \ref skip_lu_steps are impossible to tell apart from the different simulations. + */ template Rosenbrock::Stats::Stats& Rosenbrock::Stats::operator+=(const Stats& other) { @@ -84,6 +118,19 @@ namespace Integrator return *this; } + /** + * @brief Checks to see if the \ref asum_ variable can be re-used from the previous stage. + * + * Often, a row in the Rosenbrock A matrix is the exact same as the previous row, but with an additional + * non-zero element at the end. In this case, the \ref asum_ variable is the exact same as the previous stage, + * but with one extra additional term. The integrator can take advantage of this and reduce a matmul for computing + * \ref asum_ down to a single `axpy`. + * + * Typically, \ref asum_ is only initialized if it needs to be re-calculated from scratch. For this reason, this + * function will always return `false` for `stage == 0`, forcing \ref asum_ to be initialized for the first stage. + * + * @param stage The stage being checked. + */ template constexpr bool Rosenbrock::Tableau::can_reuse_asum(size_t stage) const { @@ -104,6 +151,18 @@ namespace Integrator } } + /** + * @brief Checks to see if the \ref asum_ variable can be re-used from the last stage to compute the output state + * + * Often, the Rosenbrock m vector is the exact same as the last row in the A matrix, but with an additional + * non-zero element at the end. In this case, the output state for a step is equal to the \ref asum_ variables from + * the last stage but with a single weighted vector added to it. The integrator can take advantage of this and reduce + * a matmul for computing the final state down to a single `axpy`. + * + * If a method has only a single stage, then \ref asum_ will not have been initialized (as it will just be equal to y0). + * Therefore, this method will return `false`. + * + */ template constexpr bool Rosenbrock::Tableau::can_reuse_asum_for_out() const { @@ -121,9 +180,21 @@ namespace Integrator return true; } + /** + * @brief Returns the index of a stage which can be used as an embedded error estimator, if that stage exists. + * + * Sometimes, Rosenbrock methods are designed in such a way where a stage can be used as an embedded error estimator. + * Typically, the embedded error estimator is a linear combination of the stages, so in this particular case the + * weights will be 1 on the estimator stage and 0 on all other stages. + * + * @pre This function is only valid to call if there is an embedded error estimator and its coefficients + * are included in this tableau. + */ template constexpr std::optional Rosenbrock::Tableau::error_estimator_stage() const { + assert(e); + std::optional re; for (size_t j = 0; j < num_stages; j++) { @@ -139,6 +210,19 @@ namespace Integrator return re; } + /** + * @brief Construct a new Rosenbrock integrator. + * + * @param tab The tableau to be used for this integrator. Since tableaus contain `std::unique_ptr`, it must be moved into the integrator. + * @param model The model to be simulated. Despite taking a pointer, this must be a valid pointer to an `Evaluator`. + * @param lin_solver The linear solver to be used when constructing stages during simulation. The reference must remain valid for as long + * as the Rosenbrock integrator lives. + * @param vector_handler The vector handler to be used when simulating. The reference must remain valid for as long as the Rosenbrock + * integrator lives. + * @param err_norm The error norm to use in calculating error for the `StepController`. Will not be accessed if the `StepController` + * does not need error (such as `FixedStep`), so `nullptr` can be passed in that circumstance. + * @param memspace The memory space that linear algebra operations should be performed in. + */ template Rosenbrock::Rosenbrock(Tableau&& tab, GridKit::Model::Evaluator* model, @@ -155,6 +239,16 @@ namespace Integrator { } + /** + * @brief Allocates memory based on the the size of \ref model_. Must be called before other methods. + * + * @note This method can fail. + * + * @pre Allocates the Jacobian matrix, which requires knowledge of the number of nonzero elements. This must be known at this point, + * so `allocate()` must be called beforehand on \ref model_ to count the nonzero elements and allocate the CSR Jacobian. + * + * @return An error code, with 0 as success. + */ template int Rosenbrock::allocate() { @@ -214,6 +308,22 @@ namespace Integrator return 0; } + /** + * @brief Initializes the simulation. Must be called before \ref integrate() or \ref time_step(). Must also be called after + * discrete events. + * + * - Sets the simulation time to `t0` and copies the initial condition from \ref model_. + * - Analyzes \ref model_ Jacobian sparsity and runs the preconditioner + * - Generates the mass matrix from \ref Evaluator::tag(). If the tag is not properly set, then initialization will fail. + * - Resets \ref stats_. + * + * @note This method can fail. + * + * @pre Must call \ref allocate() before this. + * + * @param t0 The starting simulation time. + * @return An error code, with 0 as success. + */ template int Rosenbrock::initializeSimulation(RealT t0) { @@ -249,6 +359,16 @@ namespace Integrator return 0; } + /** + * @brief Test + * + * @param out_times + * @param step_controller + * @param params + * @param out_cb + * @param step_cb + * @return int + */ template int Rosenbrock::integrate(const std::vector& out_times, StepController& step_controller, @@ -419,27 +539,86 @@ namespace Integrator return 0; } + /** + * @brief Advance the simulation forward by one step, storing the new state in \ref y_new_. + * + * Apply the Rosenbrock scheme using the stored tableau. Each stage \f(u_i\f) is calculated as + * + * \f[\left(J - \frac{1}{h\gamma} M\right)u_i = -f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j,\f] + * + * and the next state \f(y_1\f) is calculated as + * + * \f[y_1 = y_0 + \sum_{j = 1}^sm_ju_j\f] + * + * where the coefficients \f(\gamma, \alpha_i, a_{ij}, c_{ij}, m_j\f) come from the tableau, and the mass matrix \f(M\f) and Jacobian \f(J\f) come from the model. + * + * This method uses some state which is maintained between calls for future calls to `time_step()` and to communicate with \ref integrate(): + * - \ref y_cur_ is used as \f(y_0\f) and \ref y_new_ is used as \f(y_1\f) + * - \ref asum_ is used as \f(y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\f) for every stage except the first. Often, \f(a_{ij} = a_{i-1,j}\f), so this variable + * can be re-used between stages to save computation. Similarly, often \f(a_{ij} = m_j\f), so this variable can be re-used for computing \ref y_new_. + * - \ref csum_ is used as \f(M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f) for every stage except the first + * - \ref RHS_ is used as \f(f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) + M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f), i.e. + * the residual evaluated at \ref asum_ plus \ref csum_, for every stage but the first. + * - \ref RHS_first_stage_ is used as a special \ref RHS_ for the first stage, since it may need to be saved for the next step for the \ref skip_f_ flag. + * Since \f(\alpha_1 = 0\f) always, this will have a value of \f(f \left(t_0, y_0\right)\f). + * - \ref stages_ stores all of the stages \f(u_i\f). These stages are necessary to be used in future calls to \ref error_estimate() and \ref calc_dense_coeff(). + * - \ref skip_lu_ is used as a flag set by \ref integrate() to indicate that it is appropriate to use a time-delay Jacobian by re-using the factorization + * of the last step. + * - \ref skip_f_ is used as a flag set by \ref integrate() to indicate that \f(t_0, y_0\f) have not changed since the last time `time_step()` was called, + * so \ref RHS_first_stage_ can be re-used from the previous step. This will only happen when a step was rejected, so \ref skip_lu_ should always be false, + * and the entire first stage can't be re-used. + * - \ref jacobian_analyzed_ keeps track of whether the Jacobian has been factored in a previous call to `time_step()`. If so, then it can be re-factored + * in a faster way. The first factor must be done on actual data, so it cannot be performed pre-simulation. + * + * @pre Must call \ref initializeSimulation() beforehand. + * + * @note This method can fail. + * + * @todo This currently does not work with non-autonomous models. Some thought needs to be put in for how we want non-autonomous models to work. + * + * @todo It doesn't really make sense to pass `t0` here, which may be inconsistent with \ref current_time_. + * This function should just use \ref current_time_ in place of `t0`. + * + * @todo It doesn't really make sense for this method to be `public` since it relies on proper setup via \ref integrate(). + * + * @todo May be able to move the copying of model jacobian pointers to \ref allocate(). + * + * @todo Since \ref csum_ is multiplied by the mass matrix (which is currently diagonal with 0s for algebraic components), we can save some computation + * by only computing the differential parts of \ref csum_ and storing it in the same variable as \ref RHS_. + * + * @param t0 \f(t_0\f) in the above formula + * @param dt \f(h\f) in the above formula. The next state \f(y_1\f) will be an estimate of the state at \f(t_1 = t_0 + h\f). + * @return An error code, with 0 as success. + */ template int Rosenbrock::time_step(double t0, double dt) { + // A flag to keep track of if y0 (stored in y_cur_) has been copied in to the model already, to avoid double-copying + // for evaluating the Jacobian and residual on stage 1 (both evaluated at y0). bool y0_copied = false; - // Form the left-hand side of the system - // Can sometimes be skipped if the method is a w-method + // Form the left-hand side of the system. This is constant between stages. + // Can sometimes be skipped if the method allows for time-delay Jacobians (such as w-methods). [[likely]] if (!tab_.is_w || !skip_lu_) { BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); y0_copied = true; + + // GridKit, like IDA, expects to evaluate the Jacobian J = df/dy + alpha * df/dy', + // so we need a negative here since df/dy' = M. model_->updateTime(t0, -1.0 / (dt * tab_.gamma)); BUBBLE_FAIL(model_->evaluateJacobian()); GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); + + // TODO: This can likely be moved to allocate? These pointers should be consistent throughout the simulation BUBBLE_FAIL(workspace_.jacobian_->setDataPointers( model_jacobian->getRowData(), model_jacobian->getColData(), model_jacobian->getValues(), memspace_)); + // We must factorize first (slower) and then can re-factorize (faster) on later steps [[likely]] if (workspace_.jacobian_analyzed_) { diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 39fce01ca..dce429823 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -33,8 +33,7 @@ namespace Integrator * the next state and re-step with the new `step_size`. * */ - bool accept; - + bool accept; /** * @brief The step size the next step should take. * @@ -60,12 +59,11 @@ namespace Integrator * @return StepControl */ virtual StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) = 0; - /** * @brief Return whether or not the `nextStep` method implementation uses the `err` parameter. If `false`, this parameter is not calculated. * */ - virtual bool usesError() const = 0; + virtual bool usesError() const = 0; }; /** @@ -113,48 +111,41 @@ namespace Integrator * */ double sim_time; - /** * @brief The size of the step. * */ double step_size; - /** * @brief The size of the next step, as governed by the current `StepController` in use. * */ double next_step_size; - /** * @brief The estimated error made by the step, as calculated by the current `ErrorNorm` in use. * */ double err_est; - /** * @brief The step number, starting at 1. * */ size_t step_no; - /** * @brief Whether or not the integrator decided to skip computing the decomposition of the Jacobian on this step. * */ - bool skip_lu; - + bool skip_lu; /** * @brief Whether or not the integrator decided to skip evaluating the residual on the first stage on this step. * */ - bool skip_f; - + bool skip_f; /** * @brief Whether or not this step was accepted by the `StepController` in use. * */ - bool accepted; + bool accepted; std::string csv_report() const; std::string report() const; @@ -171,54 +162,46 @@ namespace Integrator * */ std::vector rejections; - /** * @brief Information of each step which the integrator decided to skip re-factoring the Jacobian. * */ std::vector skip_lu_steps; - /** * @brief How many steps the integrator has taken. * */ - size_t num_steps = 0; - + size_t num_steps = 0; /** * @brief Number of model residual function evaluations. * */ - size_t f_evals = 0; - + size_t f_evals = 0; /** * @brief Number of model residual function evaluations which have been skipped by the integrator. * */ - size_t f_skipped = 0; - + size_t f_skipped = 0; /** * @brief Number of model Jacobian evaluations. * */ - size_t jac_evals = 0; - + size_t jac_evals = 0; /** * @brief Number of linear solves against the model Jacobian. * */ - size_t decomp_solves = 0; - + size_t decomp_solves = 0; /** * @brief Minimum step size. * */ - double min_step = INFINITY; - + double min_step = INFINITY; /** * @brief Maximum step size. * */ - double max_step = 0; + double max_step = 0; std::string report() const; Stats& operator+=(const Stats& other); @@ -236,14 +219,12 @@ namespace Integrator * @todo Consider adding a starting step size selector to select this automatically. */ double starting_step = 1e-5; - /** * @brief The maximum number of steps the integrator should take. If the integrator has not reached the final time before * taking this many steps, then integration is stopped. For more details, see `integrate()`. * */ - size_t max_steps = 2000; - + size_t max_steps = 2000; /** * @brief Whether or not the integrator should attempt to skip Jacobian decompositions. * @@ -251,7 +232,7 @@ namespace Integrator * the time taken to compute each step. However, the overall number of steps taken will increase. * */ - bool skip_lu = false; + bool skip_lu = false; }; /** @@ -266,14 +247,12 @@ namespace Integrator * @brief The number of stages used by the method. Each stage requires one model residual evaluation. * */ - size_t num_stages; - + size_t num_stages; /** * @brief The coefficient along the diagonal of the Gamma matrix. * */ - RealT gamma; - + RealT gamma; /** * @brief A vector of sums of rows of the alpha matrix. These are the classic * Runge-Kutta 'c' coefficients, or abscissae. The size of this vector @@ -281,42 +260,36 @@ namespace Integrator * */ std::unique_ptr alpha_sum; - /** * @brief A vector of sums of rows of the Gamma matrix. The size of this vector * should be equal to `num_stages`. * */ std::unique_ptr gamma_sum; - /** * @brief A vector of weights for constructing the final solution from the stages. * The size of this vector should be equal to `num_stages`. * */ std::unique_ptr m; - /** * @brief OPTIONAL vector of coefficients for the embedded error method. If it exists, * the size of this vector should be equal to `num_stages`. * */ std::unique_ptr e; - /** * @brief The transformed A coefficient matrix. Strictly lower triangular and stored in dense row-major form. * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. * */ std::unique_ptr A; - /** * @brief The transformed C coefficient matrix. Strictly lower triangular and stored in dense row-major form. * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. * */ std::unique_ptr C; - /** * @brief OPTIONAL matrix of dense coefficients. Defines how the stages should be transformed into interpolant * nodes for computing dense output. The interpolating polynomial has an order one less than the order of @@ -325,7 +298,6 @@ namespace Integrator * */ std::unique_ptr H; - /** * @brief What ODE order these coefficients satisfy. If `is_dae` is true, then the coefficients must additionally satisfy * DAE conditions up to this order. If `is_w` is true, then the coefficients must additionally satisfy ROW conditions @@ -334,27 +306,24 @@ namespace Integrator */ uint8_t order; - /** * @brief Whether or not these coefficients are appropriate to use in a Rosenbrock-Krylov (ROK) solver. * */ - bool is_krylov; - + bool is_krylov; /** * @brief Whether or not these coefficients satisfy Rosenbrock-W (ROW) order conditions up to `order`. * The integrator may take advantage of this fact by e.g. using time-delay Jacobians to speed up computation. * */ - bool is_w; - + bool is_w; /** * @brief Whether or not these coefficients satisfy DAE order conditions up to `order`. If this is not true, * these coefficients should not be used to solve models with algebraic conditions (indicated by a * `Model::Evaluator::tag_` value of 0). * */ - bool is_dae; + bool is_dae; /** * @brief Whether or not this tableau contains an embedded error estimator method. @@ -397,15 +366,13 @@ namespace Integrator * the initial step after resuming. * */ - double step_size_ = 0; - + double step_size_ = 0; /** * @brief The step size of the previous step. Used for operations which need to be done on the current step, but step size * control for the next step has already been performed. * */ - double prev_step_size_ = 0; - + double prev_step_size_ = 0; /** * @brief Whether or not the integrator should attempt to skip Jacobian decomposition on the next step. Controlled by the * time stepping algorithm in \link integrate() \endlink . Generally, this should only be set if we suspect the Jacobian for the @@ -414,60 +381,55 @@ namespace Integrator * as the previous step. * */ - bool skip_lu_ = false; - + bool skip_lu_ = false; /** * @brief Whether or not the integrator should attempt to skip the residual function evaluation of the first stage on the * next step. This should only be used when a step is rejected and the residual function is evaluated at the exact * same arguments as the previous step. Then \ref RHS_first_stage_ can be re-used rather than re-calculated. * */ - bool skip_f_ = false; - + bool skip_f_ = false; /** * @brief Keeps track of whether or not the integrator currently has valid dense coefficients. * i.e. they have been computed and haven't been invalidated by taking another step. This can be used to avoid * re-computing dense coefficients when interpolating states multiple times in one step. * */ - bool dense_coefficients_valid_ = false; + bool dense_coefficients_valid_ = false; /** * @brief The tableau of Rosenbrock coefficients currently being used by the integrator. * */ - Tableau tab_; - + Tableau tab_; /** * @brief The model being simulated. * */ GridKit::Model::Evaluator* model_; - /** * @brief The linear solver to be used during integration in \link time_step() \endlink. * */ - ReSolve::SystemSolver& lin_solver_; + ReSolve::SystemSolver& lin_solver_; /** * @brief The vector handler to be used for vector operations by the integrator. * */ - ReSolve::VectorHandler& vector_handler_; - + ReSolve::VectorHandler& vector_handler_; /** * @brief The `ErrorNorm` to be used by the `StepController` in \link integrate() \endlink. * * @note Can be `nullptr` if no `ErrorNorm` is configured, in case the `StepController` does not need an error to be calculated. * + * @todo Should be removed from the `Rosenbrock` class. Whether or not this is needed is dependent on the `StepController`, so it should be stored there. */ - const ErrorNorm* err_norm_; - + const ErrorNorm* err_norm_; /** * @brief The memory space where linear algebra operations hsould be done in. * */ - ReSolve::memory::MemorySpace memspace_; + ReSolve::memory::MemorySpace memspace_; /** * @brief The current simulation time. @@ -480,19 +442,16 @@ namespace Integrator * */ std::unique_ptr y_prev_; - /** * @brief The state on the current step. * */ std::unique_ptr y_cur_; - /** * @brief The incoming state for the next step. * */ std::unique_ptr y_new_; - /** * @brief Used as output for dense output interpolation. * @@ -544,19 +503,16 @@ namespace Integrator * */ std::unique_ptr asum_; - /** * @brief Sum of C-weighted states used in linearization in a stage. * */ std::unique_ptr csum_; - /** * @brief Right-hand side of linear solve used in a stage. * */ std::unique_ptr RHS_; - /** * @brief Right-hand side of linear solve used in the first stage. * Stored separately from \ref RHS_ since it can be re-used by future steps if the current step is rejected. @@ -565,19 +521,16 @@ namespace Integrator * */ std::unique_ptr RHS_first_stage_; - /** * @brief Time Jacobian for non-autonomous models. Currently unused, since models are assumed to be autonomous. * */ std::unique_ptr dFdt_; - /** * @brief Vector representation of a diagonal mass matrix. * */ std::unique_ptr mass_; - /** * @brief Estimated error produced by a step in a method with an empbedded error estimator. * @@ -653,8 +606,7 @@ namespace Integrator * @note Should be between 0 and 1. * */ - double facmin = 0.2; - + double facmin = 0.2; /** * @brief The maximum multiple by which the step size can be multiplied to obtain the new step size. * Decreasing this will make the integrator more conservative in selecting the step size - @@ -663,8 +615,7 @@ namespace Integrator * @note Should be greater than 1. * */ - double facmax = 5.0; - + double facmax = 5.0; /** * @brief A "fudge factor" introduced to decrease risk of failing a step. The larger the fudge factor, * the more likely steps will fail, but fewer steps will be taken. @@ -718,20 +669,56 @@ namespace Integrator } }; + /** + * @brief The infinity error norm, which requires the error in every component in the system + * to meet tolerance. + * + */ class InfNorm : public ErrorNorm { + /** + * @brief A workspace for the linear algebra operations required to calculate the norm. + * + */ mutable struct { + /** + * @brief The final vector which will have its norm taken. + * + */ std::unique_ptr out_; + /** + * @brief The vector which will be used to scale the error in accordance with the tolerances. + * + */ std::unique_ptr scale_; + /** + * @brief The absolute value of yprev. Used to calculate \ref scale_ + * + */ std::unique_ptr yprev_abs_; } workspace_; public: + /** + * @brief The configurable parameters of the error norm. + * + */ struct Parameters { + /** + * @brief A vector of absolute tolerances for each component. The norm will attempt to reject errors + * in each component above this tolerance. + * + */ std::unique_ptr atol; - double rtol; + + /** + * @brief The relative tolerance. The norm will attempt to reject any error larger in percentage of + * the solution's maximum element than this. + * + */ + double rtol; } params_; InfNorm(Parameters&& params) : params_(std::move(params)) diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 0b0368213..911ed19ea 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -9,6 +9,7 @@ if(${DOXYGEN_FOUND}) set(DOXYGEN_SOURCE_BROWSER YES) set(DOXYGEN_INTERACTIVE_SVG YES) set(DOXYGEN_DISTRIBUTE_GROUP_DOC YES) + set(DOXYGEN_USE_MATHJAX YES) doxygen_add_docs(GridKitDocs ${CMAKE_SOURCE_DIR}/GridKit ${CMAKE_SOURCE_DIR}/README.md) endif() From 08f100b1a91a5a15e4ae283427d5d3927176b501 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 23 Mar 2026 14:08:01 -0400 Subject: [PATCH 20/57] More documentation --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 184 ++++++++++++++++++++++---- GridKit/Solver/Dynamic/Rosenbrock.hpp | 3 + 2 files changed, 161 insertions(+), 26 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index c4f9ce3ef..a6895b8b2 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -319,7 +319,7 @@ namespace Integrator * * @note This method can fail. * - * @pre Must call \ref allocate() before this. + * @pre Must have called \ref allocate(). * * @param t0 The starting simulation time. * @return An error code, with 0 as success. @@ -360,14 +360,41 @@ namespace Integrator } /** - * @brief Test - * - * @param out_times - * @param step_controller - * @param params - * @param out_cb - * @param step_cb - * @return int + * @brief Simulate the given model, producing output at the given output times. + * + * Implements a simple time stepping algorithm and facilitates output. + * 1. Calls \ref time_step() to move the simulation forward by \ref step_size_ (starting at `params.starting_step`). + * 2. Consults the `step_controller` to see if the step is accepted, and what the next \ref step_size_ should be. + * 3. If accepted, advance simulation by adding \ref step_size_ to \ref current_time_ using Kahan summation and shifting + * \ref y_new_ -> \ref y_cur_ -> \ref y_prev_. + * 4. Any time we step over an output time, calculate dense coefficients if available. + * 5. For each output time stepped over in the last step, interpolate the output at that time and call `out_cb`. + * + * Time is advanced to at least the final output time. The simulation may step over the final time, so if you + * wish to restart simulation from the final output time, make sure to re-initialize the model's state and call + * \ref initializeSimulation(). + * + * @note This method can fail. + * + * @pre Must have called \ref initializeSimulation(). + * + * @todo A higher-order Hermite interpolation can be used as a fallback for interpolation when dense coefficients don't exist. + * Each step needs to sample the residual function at the beginning (and therefore end) of the step, which contains derivative + * information. This derivative information can be re-used for Hermite interpolation. + * + * @todo It doesn't really make sense to have the error estimator separate from the step controller, since your choice of one will + * affect your choice of the other. The error estimator should probably be inside the step controller, then accessed if needed. + * + * @param out_times The times at which output is wanted. The simulation will stop once the final output time has been reached. + * @param step_controller The step size controller to use during the simulation. + * @param params The parameters to use during the simulation. + * @param out_cb An optional function which, if provided, will be called once for each time in `out_times`. The simulation time of the + * output is passed as the only argument. Before being called, the model will be updated with the output state and can be queried + * separately by the callback if needed. + * @param step_cb An optional function which, if provided, will be called once for each step the integrator takes. Information about the + * step which was just taken is provided as the argument. Before being called, the model will be updated with the current value of the state + * an can be queried separately by the callback if needed. Useful for debugging simulations. + * @return An error code, with 0 as success. */ template int Rosenbrock::integrate(const std::vector& out_times, @@ -388,6 +415,7 @@ namespace Integrator // later double time_buffer = 0; + // Generate output for each output time for (double out_time : out_times) { while (current_time_ < out_time && stats_.num_steps < params.max_steps) @@ -429,8 +457,9 @@ namespace Integrator time_buffer = step_size_adj - (next_time - current_time_); current_time_ = next_time; - // step_cb(current_time, yout); - skip_f_ = false; + // Since time is advancing, we need to re-evaluate the residual function and dense coefficients + skip_f_ = false; + dense_coefficients_valid_ = false; stats_.num_steps++; if (skip_lu_) @@ -451,7 +480,6 @@ namespace Integrator std::swap(y_prev_, y_cur_); std::swap(y_cur_, y_new_); - dense_coefficients_valid_ = false; } else { @@ -469,6 +497,8 @@ namespace Integrator }); } + // Check if we can use time delay Jacobians. If we would have increased the step size (but not too much), + // instead keep the step size the same and use time-delay Jacobian. double step_gain = next_step_size / step_size_; if (params.skip_lu && step_gain >= 1 && step_gain <= 1.2) { @@ -481,6 +511,7 @@ namespace Integrator step_size_ = next_step_size; } + // If there is a step_cb, update the model state and call it if (step_cb) { BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); @@ -499,8 +530,11 @@ namespace Integrator } } + // Check to make sure integration was paused because we reached an output time. + // Other reasons, like hitting max step count, shouldn't generate output. if (current_time_ >= out_time) { + // Generate output at the appropriate time. if (tab_.hasDenseOutput()) { if (!dense_coefficients_valid_) @@ -521,6 +555,7 @@ namespace Integrator vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); } + // Update model with output and call out_cb if it exists if (out_cb) { BUBBLE_FAIL(y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_)); @@ -557,8 +592,9 @@ namespace Integrator * - \ref asum_ is used as \f(y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\f) for every stage except the first. Often, \f(a_{ij} = a_{i-1,j}\f), so this variable * can be re-used between stages to save computation. Similarly, often \f(a_{ij} = m_j\f), so this variable can be re-used for computing \ref y_new_. * - \ref csum_ is used as \f(M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f) for every stage except the first - * - \ref RHS_ is used as \f(f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) + M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f), i.e. - * the residual evaluated at \ref asum_ plus \ref csum_, for every stage but the first. + * - \ref RHS_ is used as \f(-f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f), i.e. + * the residual evaluated at \ref asum_ plus \ref csum_, for every stage but the first. This is used as the right-hand side vector for the linear solve to solve + * for the stage value \f(u_i\f). * - \ref RHS_first_stage_ is used as a special \ref RHS_ for the first stage, since it may need to be saved for the next step for the \ref skip_f_ flag. * Since \f(\alpha_1 = 0\f) always, this will have a value of \f(f \left(t_0, y_0\right)\f). * - \ref stages_ stores all of the stages \f(u_i\f). These stages are necessary to be used in future calls to \ref error_estimate() and \ref calc_dense_coeff(). @@ -728,9 +764,31 @@ namespace Integrator return 0; } + /** + * @brief Calculates an estimation of the error produced by the last call to \ref time_step() which can be used as the + * `err` argument for \ref ErrorNorm::errorNorm(). + * + * Calculate the embedded error as + * + * \f[\hat{e} = \sum_{j = 1}^s e_j u_j,\f] + * + * where \f(e_j\f) are tableau coefficients and \f(u_j\f) are stages computed by \ref time_step(). It happens often that + * \f(e_j = 0\f) for all but one stage, where \f(e_j = 1\f). In that case, the stage itself is used as the error estimate, + * and extra calculation can be avoided. For this reason, a reference to the estimate is returned to avoid an unnecessary copy. + * + * @pre Must call \ref time_step(), and tableau must have coefficients for an embedded error estimator. + * @note This method can fail. + * + * @todo This function is fallible, but the return type makes it difficult to return an error code. Right now it will throw an + * error, but it should be refactored to allow returning an error code, such as by returning `std::variant`. + * + * @return A reference to the estimated error. + */ template State& Rosenbrock::error_estimate() const { + // Test to see if the tableau allows us to use a stage as the error estimate, + // avoiding extra computation. std::optional err_stage = tab_.error_estimator_stage(); if (err_stage) @@ -747,7 +805,7 @@ namespace Integrator throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); } - vector_handler_.scal(tab_.e[0], workspace_.stages_[0].get(), memspace_); + vector_handler_.scal(tab_.e[0], workspace_.err_est_.get(), memspace_); for (size_t j = 1; j < tab_.num_stages; j++) { if (tab_.e[j] != 0.0) @@ -760,6 +818,18 @@ namespace Integrator } } + /** + * @brief Calculate the interpolation nodes used by \ref interp_dense() based on \ref stages_ computed by + * the last call to \ref time_step(). Nodes are stored in \ref dense_coeff_. Only needs to be called once, + * but can be invalidated by future calls to \ref time_step(). \ref integrate() keeps track of \ref dense_coefficients_valid_, + * which tells you if this function is needed to be called. + * + * @pre Must call \ref time_step() first. + * + * @note This method can fail. + * + * @return An error code, with 0 as success. + */ template int Rosenbrock::calc_dense_coeff() { @@ -782,10 +852,37 @@ namespace Integrator return 0; } + /** + * @brief Calculate an interpolated state at \f(\theta = \frac{t - t_0}{h}\f) between the initial state + * \f(y_0\f) and final state \f(y_1\f) of the last step taken using dense interpolation nodes calculated + * by \ref calc_dense_coeff(). + * + * For a valid interpolation of appropriate order, \f(\theta\f) must be in \f([0, 1]\f), although values + * beyond 1 can be used for extrapolation if desired. + * + * Uses a number of interpolation nodes equal to the order. Since \f(y_0\f) and \f(y_1\f) are interpolation + * nodes, this method will only access \ref dense_coeff_ if the method's order is greater than 2. + * + * The inteporlation is calculated as + * + * \f[y(\theta) = (1 - \theta) y_0 + \theta \left(y_1 + (1 - \theta) \sum_{i = 1}^{p-2} \theta^{i-1} \hat{y}_i\right),\f] + * + * where \f(\hat{y}_i\f) are the dense interpolation nodes calculated in \ref calc_dense_coeff() and \f(p\f) is the order of the method. + * This calculation is carried out using synthetic division. + * + * @pre Must call \ref calc_dense_coeff() first. + * + * @note This method can fail. + * + * @note If `theta` is very close to 0 or very close to 1, it might be better to simply use \ref y_prev_ or \ref y_cur_ instead of + * calculating this interpolation. + * + * @param theta The fraction of time during the last step taken to calculate the interpolation at. \f(\theta = \frac{t - t_0}{h}\f) + * @return An error code, with 0 as success. + */ template int Rosenbrock::interp_dense(double theta) { - double one = 1.0; if (tab_.order > 2) { BUBBLE_FAIL(y_interp_->copyFromExternal(dense_coeff_[tab_.order - 3].get(), memspace_, memspace_)); @@ -793,24 +890,31 @@ namespace Integrator for (size_t i = 1; i < tab_.order - 2; i++) { vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(one, dense_coeff_[tab_.order - 3 - i].get(), y_interp_.get(), memspace_); + vector_handler_.axpy(1.0, dense_coeff_[tab_.order - 3 - i].get(), y_interp_.get(), memspace_); } + + // TODO: This scal can be removed and absorbed into the next axpy, except that it currently isn't possible to put a scalar + // multiple on the y term. + vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); } else { BUBBLE_FAIL(y_interp_->setToZero(memspace_)); } - double omt = 1 - theta; - - vector_handler_.scal(omt, y_interp_.get(), memspace_); - vector_handler_.axpy(one, y_cur_.get(), y_interp_.get(), memspace_); + vector_handler_.axpy(1.0, y_cur_.get(), y_interp_.get(), memspace_); vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(omt, y_prev_.get(), y_interp_.get(), memspace_); + vector_handler_.axpy(1 - theta, y_prev_.get(), y_interp_.get(), memspace_); return 0; } + /** + * @brief Standard textbook adaptive controller. Accept if `err <= 1` and use + * + * \f[h_{new} = h * \min \left\{fac_{max}, \max\left\{fac_{min}, fac_{scale} \cdot e ^{-1/p}\right\}\right\}.\f] + * + */ StepControl AdaptiveStep::nextStep(double err, StepControl prev_step, uint8_t method_order) { StepControl next_step = prev_step; @@ -823,7 +927,11 @@ namespace Integrator return next_step; } - StepControl FixedStep::nextStep([[maybe_unused]] double err, [[maybe_unused]] StepControl prev_step, [[maybe_unused]] uint8_t method_order) + /** + * @brief Fixed step - accept every step, no matter the error, and keep the step size the same. + * + */ + StepControl FixedStep::nextStep([[maybe_unused]] double err, StepControl prev_step, [[maybe_unused]] uint8_t method_order) { return StepControl{ .accept = true, @@ -831,11 +939,35 @@ namespace Integrator }; } + /** + * @brief Calculate the infinity error norm as + * + * \f[e = \max\left\{\frac{|\hat{e}_i|}{Atol_i + Rtol \cdot \max\{|y_{0i}|, |y_{1i}|\}}\right\}_i,\f] + * + * where \f(y_0\f) is the initial state, \f(y_1\f) is the next state, and \f(\hat{e}\f) is the estimated error made in calculating + * the next state (typically \f(\hat{e} = y_1 - \hat{y}_1\f) for some different-order approximation \f(\hat{y}_1\f)). + * + * @param err \f(\hat{e}\f) in the above formula. + * @param y \f(y_1\f) in the above formula. + * @param yprev \f(y_0\f) in the above formula. + * @param handler The handler to be used for performing linear algebra operations. + * @param memspace The memory space to be used for performing linear lagebra operations. + * @see `Rosenbrock::error_estimate()` + */ double InfNorm::errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const { - workspace_.out_->copyFromExternal(&err, memspace, memspace); - workspace_.scale_->copyFromExternal(&y, memspace, memspace); - workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace); + if (int err_code = workspace_.out_->copyFromExternal(&err, memspace, memspace)) + { + throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + } + if (int err_code = workspace_.scale_->copyFromExternal(&y, memspace, memspace)) + { + throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + } + if (int err_code = workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace)) + { + throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + } handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index dce429823..f33320fac 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -45,6 +45,7 @@ namespace Integrator * @brief Interface for step size controllers. Used by `Rosenbrock` integrators to decide when to accept/reject steps and * what size each step should be. * + * @todo It may be best to have \ref usesError() return a reference to the \ref ErrorNorm that should be used. */ class StepController { @@ -83,6 +84,8 @@ namespace Integrator * @param handler A vector handler which can be used to facilitate vector operations. * @param memspace The memory space which vector operations should be performed in/. * @return double The error. + * + * @todo Allow this method to fail, since it will likely involve linear algebra calls. */ virtual double errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const = 0; }; From 7748003664bdd32f2e215972246c7b36c679dad9 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 23 Mar 2026 14:41:46 -0400 Subject: [PATCH 21/57] Update ReSolve dependency --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index a6895b8b2..928281b8d 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -972,9 +972,12 @@ namespace Integrator handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.yprev_abs_.get(), memspace); + + // TODO: This scal shouldn't be necessary, but axpy doesn't support scaling the y parameter. In the future, + // the scaling should be able to be put on the next axpy. handler.scal(params_.rtol, workspace_.scale_.get(), memspace); handler.axpy(1.0, params_.atol.get(), workspace_.scale_.get(), memspace); - handler.scaleInv(workspace_.scale_.get(), workspace_.out_.get(), memspace); + handler.diagSolve(workspace_.scale_.get(), workspace_.out_.get(), memspace); return handler.amax(workspace_.out_.get(), memspace); } From c84a7c193eab7db1bfddf1db7e27b9d8003eafbc Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 24 Mar 2026 10:46:09 -0400 Subject: [PATCH 22/57] Code review documentation changes --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 7 ++++++- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 928281b8d..f55000ae1 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -215,6 +215,7 @@ namespace Integrator * * @param tab The tableau to be used for this integrator. Since tableaus contain `std::unique_ptr`, it must be moved into the integrator. * @param model The model to be simulated. Despite taking a pointer, this must be a valid pointer to an `Evaluator`. + * Must have \ref Evaluator::tag() set, and must be in Hessenberg form (\f(F(\dot y, y) = \dot y - f(y)\f)). * @param lin_solver The linear solver to be used when constructing stages during simulation. The reference must remain valid for as long * as the Rosenbrock integrator lives. * @param vector_handler The vector handler to be used when simulating. The reference must remain valid for as long as the Rosenbrock @@ -319,7 +320,9 @@ namespace Integrator * * @note This method can fail. * - * @pre Must have called \ref allocate(). + * @pre Must have called \ref allocate(). The `model.tag_` variable must be properly constructed. + * + * @todo Document mass matrix construction * * @param t0 The starting simulation time. * @return An error code, with 0 as success. @@ -385,6 +388,8 @@ namespace Integrator * @todo It doesn't really make sense to have the error estimator separate from the step controller, since your choice of one will * affect your choice of the other. The error estimator should probably be inside the step controller, then accessed if needed. * + * @todo Return an error when max steps is hit. + * * @param out_times The times at which output is wanted. The simulation will stop once the final output time has been reached. * @param step_controller The step size controller to use during the simulation. * @param params The parameters to use during the simulation. diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp index 9ed39622d..d167bfa16 100644 --- a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -38,6 +38,12 @@ namespace Integrator return re; } + /** + * @brief + * + * @todo Add paper citation. + * + */ template Rosenbrock::Tableau Rosenbrock::Tableau::rodas5p() { From 7eba4f6c65eb8d141af7702fb561d0f23199d69c Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Thu, 26 Mar 2026 10:43:13 -0400 Subject: [PATCH 23/57] Changes from review --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index f55000ae1..16abc42d2 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -390,6 +390,10 @@ namespace Integrator * * @todo Return an error when max steps is hit. * + * @todo Check if current time is close enough to output time, and skip interpolation + * + * @todo Configure upper bound for skip lu step size increase + * * @param out_times The times at which output is wanted. The simulation will stop once the final output time has been reached. * @param step_controller The step size controller to use during the simulation. * @param params The parameters to use during the simulation. @@ -504,6 +508,7 @@ namespace Integrator // Check if we can use time delay Jacobians. If we would have increased the step size (but not too much), // instead keep the step size the same and use time-delay Jacobian. + // TODO: configure upper bound here double step_gain = next_step_size / step_size_; if (params.skip_lu && step_gain >= 1 && step_gain <= 1.2) { @@ -539,6 +544,10 @@ namespace Integrator // Other reasons, like hitting max step count, shouldn't generate output. if (current_time_ >= out_time) { + // Theta = (t - t0) / h = (t - t1) / h + 1 + // current_time_ is t1 here + double theta = (out_time - current_time_) / prev_step_size_ + 1; + // Generate output at the appropriate time. if (tab_.hasDenseOutput()) { @@ -548,13 +557,11 @@ namespace Integrator dense_coefficients_valid_ = true; } - double theta = (out_time - current_time_) / prev_step_size_ + 1; BUBBLE_FAIL(interp_dense(theta)); } else { // TODO: Put code for alternative interpolation (Abdou) here - double theta = (out_time - current_time_) / prev_step_size_ + 1; BUBBLE_FAIL(y_interp_->copyFromExternal(y_prev_.get(), memspace_, memspace_)); vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); @@ -736,6 +743,7 @@ namespace Integrator BUBBLE_FAIL(model_->evaluateResidual()); workspace_.RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); + // TODO: examine if this -1 is correct vector_handler_.scal(-1, workspace_.RHS_.get(), memspace_); vector_handler_.scal(workspace_.mass_.get(), workspace_.csum_.get(), memspace_); vector_handler_.axpy(-1, workspace_.csum_.get(), workspace_.RHS_.get(), memspace_); @@ -947,7 +955,7 @@ namespace Integrator /** * @brief Calculate the infinity error norm as * - * \f[e = \max\left\{\frac{|\hat{e}_i|}{Atol_i + Rtol \cdot \max\{|y_{0i}|, |y_{1i}|\}}\right\}_i,\f] + * \f[e = \max_i\frac{|\hat{e}_i|}{Atol_i + Rtol \cdot \max\{|y_{0i}|, |y_{1i}|\}},\f] * * where \f(y_0\f) is the initial state, \f(y_1\f) is the next state, and \f(\hat{e}\f) is the estimated error made in calculating * the next state (typically \f(\hat{e} = y_1 - \hat{y}_1\f) for some different-order approximation \f(\hat{y}_1\f)). @@ -976,7 +984,7 @@ namespace Integrator handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); - handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.yprev_abs_.get(), memspace); + handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.scale_.get(), memspace); // TODO: This scal shouldn't be necessary, but axpy doesn't support scaling the y parameter. In the future, // the scaling should be able to be put on the next axpy. From 8cf790e72f1d72a6463d3992531d4e43064600af Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 17 Apr 2026 11:17:15 -0400 Subject: [PATCH 24/57] Fix compiler errors and warnings on g++ --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 16abc42d2..0b062ae92 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -101,7 +101,7 @@ namespace Integrator * @todo Right now, the step numbers for \ref rejections and \ref skip_lu_steps are impossible to tell apart from the different simulations. */ template - Rosenbrock::Stats::Stats& Rosenbrock::Stats::operator+=(const Stats& other) + typename Rosenbrock::Stats& Rosenbrock::Stats::operator+=(const Stats& other) { rejections.insert(rejections.end(), other.rejections.begin(), other.rejections.end()); skip_lu_steps.insert(skip_lu_steps.end(), other.skip_lu_steps.begin(), other.skip_lu_steps.end()); @@ -297,7 +297,7 @@ namespace Integrator if (tab_.order > 2) { dense_coeff_ = std::make_unique[]>(tab_.order - 2); - for (size_t i = 0; i < tab_.order - 2; i++) + for (size_t i = 0; i < static_cast(tab_.order - 2); i++) { dense_coeff_[i] = std::make_unique(size); BUBBLE_FAIL(dense_coeff_[i]->allocate(memspace_)); @@ -848,14 +848,14 @@ namespace Integrator { if (tab_.order > 2) { - for (size_t j = 0; j < tab_.order - 2; j++) + for (size_t j = 0; j < static_cast(tab_.order - 2); j++) { BUBBLE_FAIL(dense_coeff_[j]->setToZero(memspace_)); } for (size_t i = 0; i < tab_.num_stages; i++) { - for (size_t j = 0; j < tab_.order - 2; j++) + for (size_t j = 0; j < static_cast(tab_.order - 2); j++) { vector_handler_.axpy(tab_.H[j * tab_.num_stages + i], workspace_.stages_[i].get(), dense_coeff_[j].get(), memspace_); } @@ -900,7 +900,7 @@ namespace Integrator { BUBBLE_FAIL(y_interp_->copyFromExternal(dense_coeff_[tab_.order - 3].get(), memspace_, memspace_)); - for (size_t i = 1; i < tab_.order - 2; i++) + for (size_t i = 1; i < static_cast(tab_.order - 2); i++) { vector_handler_.scal(theta, y_interp_.get(), memspace_); vector_handler_.axpy(1.0, dense_coeff_[tab_.order - 3 - i].get(), y_interp_.get(), memspace_); From 8cd71e073067ea6e144357fa1158b94f26b3f9e9 Mon Sep 17 00:00:00 2001 From: alexander-novo Date: Tue, 7 Jul 2026 18:21:35 +0000 Subject: [PATCH 25/57] Apply pre-commit fixes --- GridKit/Solver/Dynamic/CMakeLists.txt | 27 ++++++++++--------- GridKit/Solver/Dynamic/Rosenbrock.cpp | 2 +- GridKit/Solver/Dynamic/Rosenbrock.hpp | 6 +++-- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 2 +- tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 16 +++++------ 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index 593190bb2..88b77deae 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -43,17 +43,18 @@ else() GridKit::sparse_matrix) endif() -if (GRIDKIT_ENABLE_RESOLVE) - gridkit_add_library(rosenbrock - SOURCES - Rosenbrock.cpp - RosenbrockTableaus.cpp - HEADERS - ${_install_headers} +if(GRIDKIT_ENABLE_RESOLVE) + gridkit_add_library( + rosenbrock + SOURCES Rosenbrock.cpp RosenbrockTableaus.cpp + HEADERS ${_install_headers} LINK_LIBRARIES - PUBLIC ReSolve::ReSolve - PUBLIC GridKit::definitions - PUBLIC GridKit::utilities_logger - PUBLIC GridKit::sparse_matrix - ) -endif() \ No newline at end of file + PUBLIC + ReSolve::ReSolve + PUBLIC + GridKit::definitions + PUBLIC + GridKit::utilities_logger + PUBLIC + GridKit::sparse_matrix) +endif() diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 0b062ae92..a26166145 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -996,4 +996,4 @@ namespace Integrator } template class Rosenbrock; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index f33320fac..ea60b2647 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -630,7 +630,8 @@ namespace Integrator } params_; public: - AdaptiveStep(const Parameters& params) : params_(params) + AdaptiveStep(const Parameters& params) + : params_(params) { } @@ -724,7 +725,8 @@ namespace Integrator double rtol; } params_; - InfNorm(Parameters&& params) : params_(std::move(params)) + InfNorm(Parameters&& params) + : params_(std::move(params)) { } diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp index d167bfa16..25cfe3df8 100644 --- a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -202,4 +202,4 @@ namespace Integrator } template class Rosenbrock; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index c9467d971..5ce58132d 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -1,18 +1,16 @@ # if(TARGET SUNDIALS::idas) - add_executable(test_ida runIdaTests.cpp) - target_link_libraries( - test_ida GridKit::solvers_dyn GridKit::testing) + add_executable(test_ida runIdaTests.cpp) + target_link_libraries(test_ida GridKit::solvers_dyn GridKit::testing) - add_test(NAME IDATest COMMAND $) + add_test(NAME IDATest COMMAND $) endif() -if (GRIDKIT_ENABLE_RESOLVE) - add_executable(test_ros runRosenbrockTests.cpp) - target_link_libraries(test_ros GridKit::rosenbrock GridKit::testing) - add_test(NAME ROSTest COMMAND $) +if(GRIDKIT_ENABLE_RESOLVE) + add_executable(test_ros runRosenbrockTests.cpp) + target_link_libraries(test_ros GridKit::rosenbrock GridKit::testing) + add_test(NAME ROSTest COMMAND $) endif() - install(TARGETS test_ida RUNTIME DESTINATION bin) From b7a01cb68bcb3b52c5039d57cc7ae17c5b46dec7 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 7 Jul 2026 15:27:21 -0400 Subject: [PATCH 26/57] fix cmake issue --- tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index 5ce58132d..ca1dc1a35 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -5,12 +5,14 @@ if(TARGET SUNDIALS::idas) target_link_libraries(test_ida GridKit::solvers_dyn GridKit::testing) add_test(NAME IDATest COMMAND $) + + install(TARGETS test_ida RUNTIME DESTINATION bin) endif() if(GRIDKIT_ENABLE_RESOLVE) add_executable(test_ros runRosenbrockTests.cpp) target_link_libraries(test_ros GridKit::rosenbrock GridKit::testing) add_test(NAME ROSTest COMMAND $) -endif() -install(TARGETS test_ida RUNTIME DESTINATION bin) + install(TARGETS test_ros RUNTIME DESTINATION bin) +endif() From 9b26bdf06770be9c5453e0e9ef2d8e838be3fbb8 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Wed, 8 Jul 2026 20:07:59 -0400 Subject: [PATCH 27/57] Update Rosenbrock to use GridKit Vector and VectorHandler --- .../MemoryUtilities/ResolveMemoryUtils.hpp | 24 ++++++++++ GridKit/Solver/Dynamic/CMakeLists.txt | 2 + GridKit/Solver/Dynamic/Rosenbrock.cpp | 47 ++++++++++++------- GridKit/Solver/Dynamic/Rosenbrock.hpp | 47 +++++++++++-------- .../Solver/Dynamic/RosenbrockTests.hpp | 32 ++++++------- 5 files changed, 98 insertions(+), 54 deletions(-) create mode 100644 GridKit/MemoryUtilities/ResolveMemoryUtils.hpp diff --git a/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp b/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp new file mode 100644 index 000000000..83b93ffb9 --- /dev/null +++ b/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +namespace GridKit +{ + namespace memory + { + inline ReSolve::memory::MemorySpace memorySpaceAsResolve(MemorySpace memspace) + { + switch (memspace) + { + case HOST: + return ReSolve::memory::HOST; + case DEVICE: + return ReSolve::memory::DEVICE; + default: + throw "Memory space not supported"; + } + } + } // namespace memory +} // namespace GridKit \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index 88b77deae..88e3e5e6c 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -52,6 +52,8 @@ if(GRIDKIT_ENABLE_RESOLVE) PUBLIC ReSolve::ReSolve PUBLIC + GridKit::dense_vector + PUBLIC GridKit::definitions PUBLIC GridKit::utilities_logger diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index a26166145..8fc00447c 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -6,6 +6,8 @@ #include +#include + /** * @brief A small helper macro to "bubble" errors. The Rosenbrock implementations call many * fallible model and linear algebra functions, and often the only thing that can be @@ -225,12 +227,12 @@ namespace Integrator * @param memspace The memory space that linear algebra operations should be performed in. */ template - Rosenbrock::Rosenbrock(Tableau&& tab, - GridKit::Model::Evaluator* model, - ReSolve::SystemSolver& lin_solver, - ReSolve::VectorHandler& vector_handler, - const ErrorNorm* err_norm, - ReSolve::memory::MemorySpace memspace) + Rosenbrock::Rosenbrock(Tableau&& tab, + GridKit::Model::Evaluator* model, + ReSolve::SystemSolver& lin_solver, + GridKit::LinearAlgebra::VectorHandler& vector_handler, + const ErrorNorm* err_norm, + GridKit::memory::MemorySpace memspace) : tab_(std::move(tab)), model_(model), lin_solver_(lin_solver), @@ -266,6 +268,9 @@ namespace Integrator workspace_.dFdt_ = std::make_unique(size); workspace_.mass_ = std::make_unique(size); + resolve_rhs_ = std::make_unique(size); + resolve_lhs_ = std::make_unique(size); + BUBBLE_FAIL(y_prev_->allocate(memspace_)); BUBBLE_FAIL(y_cur_->allocate(memspace_)); BUBBLE_FAIL(y_new_->allocate(memspace_)); @@ -292,6 +297,7 @@ namespace Integrator { workspace_.stages_[i] = std::make_unique(size); BUBBLE_FAIL(workspace_.stages_[i]->allocate(memspace_)); + workspace_.stages_[i]->setToZero(memspace_); } if (tab_.order > 2) @@ -339,7 +345,7 @@ namespace Integrator model_jacobian->getRowData(), model_jacobian->getColData(), model_jacobian->getValues(), - memspace_)); + GridKit::memory::memorySpaceAsResolve(memspace_))); BUBBLE_FAIL(lin_solver_.setMatrix(workspace_.jacobian_.get())); BUBBLE_FAIL(lin_solver_.analyze()); BUBBLE_FAIL(lin_solver_.preconditionerSetup()); @@ -562,7 +568,7 @@ namespace Integrator else { // TODO: Put code for alternative interpolation (Abdou) here - BUBBLE_FAIL(y_interp_->copyFromExternal(y_prev_.get(), memspace_, memspace_)); + BUBBLE_FAIL(y_interp_->copyFromExternal(*y_prev_, memspace_, memspace_)); vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); } @@ -578,7 +584,7 @@ namespace Integrator } else { - BUBBLE_FAIL(y_interp_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); + BUBBLE_FAIL(y_interp_->copyFromExternal(*y_cur_, memspace_, memspace_)); break; } } @@ -664,7 +670,7 @@ namespace Integrator model_jacobian->getRowData(), model_jacobian->getColData(), model_jacobian->getValues(), - memspace_)); + GridKit::memory::memorySpaceAsResolve(memspace_))); // We must factorize first (slower) and then can re-factorize (faster) on later steps [[likely]] @@ -702,7 +708,9 @@ namespace Integrator stats_.f_evals++; } - BUBBLE_FAIL(lin_solver_.solve(workspace_.RHS_first_stage_.get(), workspace_.stages_[0].get())); + resolve_rhs_->setData(workspace_.RHS_first_stage_->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); + resolve_lhs_->setData(workspace_.stages_[0]->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); + BUBBLE_FAIL(lin_solver_.solve(resolve_rhs_.get(), resolve_lhs_.get())); stats_.decomp_solves++; // Rest of stages @@ -717,7 +725,7 @@ namespace Integrator } else { - BUBBLE_FAIL(workspace_.asum_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); + BUBBLE_FAIL(workspace_.asum_->copyFromExternal(*y_cur_, memspace_, memspace_)); for (size_t j = 0; j < i; j++) { @@ -748,7 +756,9 @@ namespace Integrator vector_handler_.scal(workspace_.mass_.get(), workspace_.csum_.get(), memspace_); vector_handler_.axpy(-1, workspace_.csum_.get(), workspace_.RHS_.get(), memspace_); - BUBBLE_FAIL(lin_solver_.solve(workspace_.RHS_.get(), workspace_.stages_[i].get())); + resolve_rhs_->setData(workspace_.RHS_->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); + resolve_lhs_->setData(workspace_.stages_[i]->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); + BUBBLE_FAIL(lin_solver_.solve(resolve_rhs_.get(), resolve_lhs_.get())); stats_.f_evals++; stats_.decomp_solves++; } @@ -763,7 +773,7 @@ namespace Integrator } else { - BUBBLE_FAIL(y_new_->copyFromExternal(y_cur_.get(), memspace_, memspace_)); + BUBBLE_FAIL(y_new_->copyFromExternal(*y_cur_, memspace_, memspace_)); for (size_t j = 0; j < tab_.num_stages; j++) { @@ -798,7 +808,7 @@ namespace Integrator * @return A reference to the estimated error. */ template - State& Rosenbrock::error_estimate() const + Rosenbrock::State& Rosenbrock::error_estimate() const { // Test to see if the tableau allows us to use a stage as the error estimate, // avoiding extra computation. @@ -811,7 +821,7 @@ namespace Integrator else { // TODO: could make this function return recoverable errors by using std::variant - int err_code = workspace_.err_est_->copyFromExternal(workspace_.stages_[0].get(), memspace_, memspace_); + int err_code = workspace_.err_est_->copyFromExternal(*workspace_.stages_[0], memspace_, memspace_); if (err_code) { @@ -898,7 +908,7 @@ namespace Integrator { if (tab_.order > 2) { - BUBBLE_FAIL(y_interp_->copyFromExternal(dense_coeff_[tab_.order - 3].get(), memspace_, memspace_)); + BUBBLE_FAIL(y_interp_->copyFromExternal(*dense_coeff_[tab_.order - 3], memspace_, memspace_)); for (size_t i = 1; i < static_cast(tab_.order - 2); i++) { @@ -967,7 +977,8 @@ namespace Integrator * @param memspace The memory space to be used for performing linear lagebra operations. * @see `Rosenbrock::error_estimate()` */ - double InfNorm::errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const + template + double InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const { if (int err_code = workspace_.out_->copyFromExternal(&err, memspace, memspace)) { diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index ea60b2647..59592dd9a 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -8,19 +8,18 @@ #include #include +#include +#include +#include #include #include -#include #include #include #include -#include -#include namespace Integrator { - using State = ReSolve::vector::Vector; /** * @brief Define control flow for `StepController`s to be able to control the step size of a `Rosenbrock` integrator. @@ -71,8 +70,11 @@ namespace Integrator * @brief Interface for error norms. Used to calculate the `err` parameter in `StepController::nextStep` based on a residual state error vector. * */ + template class ErrorNorm { + using State = GridKit::LinearAlgebra::Vector; + public: /** * @brief Calculate an error to be used by a step controller. Typically, an error > 1 indicates an error which does not meet tolerances, while @@ -87,7 +89,7 @@ namespace Integrator * * @todo Allow this method to fail, since it will likely involve linear algebra calls. */ - virtual double errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const = 0; + virtual double errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const = 0; }; /** @@ -101,6 +103,7 @@ namespace Integrator class Rosenbrock { using RealT = typename GridKit::ScalarTraits::RealT; + using State = GridKit::LinearAlgebra::Vector; public: /** @@ -404,22 +407,22 @@ namespace Integrator * @brief The tableau of Rosenbrock coefficients currently being used by the integrator. * */ - Tableau tab_; + Tableau tab_; /** * @brief The model being simulated. * */ - GridKit::Model::Evaluator* model_; + GridKit::Model::Evaluator* model_; /** * @brief The linear solver to be used during integration in \link time_step() \endlink. * */ - ReSolve::SystemSolver& lin_solver_; + ReSolve::SystemSolver& lin_solver_; /** * @brief The vector handler to be used for vector operations by the integrator. * */ - ReSolve::VectorHandler& vector_handler_; + GridKit::LinearAlgebra::VectorHandler& vector_handler_; /** * @brief The `ErrorNorm` to be used by the `StepController` in \link integrate() \endlink. * @@ -427,12 +430,12 @@ namespace Integrator * * @todo Should be removed from the `Rosenbrock` class. Whether or not this is needed is dependent on the `StepController`, so it should be stored there. */ - const ErrorNorm* err_norm_; + const ErrorNorm* err_norm_; /** * @brief The memory space where linear algebra operations hsould be done in. * */ - ReSolve::memory::MemorySpace memspace_; + GridKit::memory::MemorySpace memspace_; /** * @brief The current simulation time. @@ -461,6 +464,9 @@ namespace Integrator */ std::unique_ptr y_interp_; + std::unique_ptr resolve_rhs_; + std::unique_ptr resolve_lhs_; + /** * @brief Configured parameters for the integrator. * @@ -474,12 +480,12 @@ namespace Integrator Stats stats_; public: - Rosenbrock(Tableau&& tab, - GridKit::Model::Evaluator* model, - ReSolve::SystemSolver& lin_solver, - ReSolve::VectorHandler& vector_handler, - const ErrorNorm* err_norm, - ReSolve::memory::MemorySpace memspace = ReSolve::memory::HOST); + Rosenbrock(Tableau&& tab, + GridKit::Model::Evaluator* model, + ReSolve::SystemSolver& lin_solver, + GridKit::LinearAlgebra::VectorHandler& vector_handler, + const ErrorNorm* err_norm, + GridKit::memory::MemorySpace memspace = GridKit::memory::HOST); [[nodiscard("May fail. Check error code.")]] int allocate(); @@ -678,8 +684,11 @@ namespace Integrator * to meet tolerance. * */ - class InfNorm : public ErrorNorm + template + class InfNorm : public ErrorNorm { + using State = GridKit::LinearAlgebra::Vector; + /** * @brief A workspace for the linear algebra operations required to calculate the norm. * @@ -730,6 +739,6 @@ namespace Integrator { } - double errorNorm(State& err, State& y, State& yprev, ReSolve::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const final; + double errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const final; }; } // namespace Integrator diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index f56395b03..c1140c701 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -12,6 +12,7 @@ #include "GridKit/LinearAlgebra/SparseMatrix/CsrMatrix.hpp" #include #include +#include namespace GridKit { @@ -92,13 +93,19 @@ namespace GridKit return 0; } - void setTolerances([[maybe_unused]] RealT& rel_tol, [[maybe_unused]] RealT& abs_tol) const override + int setAbsoluteTolerance([[maybe_unused]] RealT rel_tol) override { + return 0; + } + + std::vector& absoluteTolerance() override + { + return abs_tol_; } - void setMaxSteps(IdxT& msa) const override + const std::vector& absoluteTolerance() const override { - msa = 2000; + return abs_tol_; } int tagDifferentiable() override @@ -256,16 +263,6 @@ namespace GridKit return f_; } - GridKit::LinearAlgebra::COO_Matrix& getJacobian() override - { - return jac_; - } - - const GridKit::LinearAlgebra::COO_Matrix& getJacobian() const override - { - return jac_; - } - std::vector& getIntegrand() override { return g_; @@ -318,7 +315,8 @@ namespace GridKit std::vector fB_; std::vector gB_; - GridKit::LinearAlgebra::COO_Matrix jac_; + std::vector abs_tol_; + std::unique_ptr> csr_jac_; RealT alpha_; @@ -347,9 +345,9 @@ namespace GridKit model.allocate(); model.initialize(); - ReSolve::LinAlgWorkspaceCpu linear_workspace; - ReSolve::SystemSolver lin_solver(&linear_workspace, "klu", "klu", "klu"); - ReSolve::VectorHandler vec_handler; + ReSolve::LinAlgWorkspaceCpu linear_workspace; + ReSolve::SystemSolver lin_solver(&linear_workspace, "klu", "klu", "klu"); + GridKit::LinearAlgebra::VectorHandler vec_handler; lin_solver.initialize(); From db7f95dd687f88f54959d4cf3ae053da4d6027a0 Mon Sep 17 00:00:00 2001 From: alexander-novo Date: Thu, 9 Jul 2026 00:08:32 +0000 Subject: [PATCH 28/57] Apply pre-commit fixes --- GridKit/MemoryUtilities/ResolveMemoryUtils.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp b/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp index 83b93ffb9..7369cd242 100644 --- a/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp +++ b/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp @@ -21,4 +21,4 @@ namespace GridKit } } } // namespace memory -} // namespace GridKit \ No newline at end of file +} // namespace GridKit From 9f7055a0b782144e1968759273956675e25ce5cb Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Wed, 8 Jul 2026 20:11:06 -0400 Subject: [PATCH 29/57] [skip ci] Update Documentation --- GridKit/MemoryUtilities/ResolveMemoryUtils.hpp | 4 ++++ GridKit/Solver/Dynamic/Rosenbrock.hpp | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp b/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp index 7369cd242..8685f90b5 100644 --- a/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp +++ b/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp @@ -8,6 +8,10 @@ namespace GridKit { namespace memory { + /** + * @brief Converts a GridKit \ref MemorySpace to its corresponding Re::Solve MemorySpace + * + */ inline ReSolve::memory::MemorySpace memorySpaceAsResolve(MemorySpace memspace) { switch (memspace) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 59592dd9a..be4e43cb8 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -464,7 +464,17 @@ namespace Integrator */ std::unique_ptr y_interp_; + /** + * @brief Re::Solve vector used for the right hand side of a linear solve. No allocation is done - + * the vector simply has its data pointer updated to point at the correct GridKit vector before the + * solve operation is called. + * + */ std::unique_ptr resolve_rhs_; + /** + * @brief Re::Solve vector used for the left hand side of a linear solve. \see resolve_rhs_ + * + */ std::unique_ptr resolve_lhs_; /** From ca9fbfbb39a75d65aab180539230a860fd15426b Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Thu, 9 Jul 2026 12:31:06 -0400 Subject: [PATCH 30/57] Add to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e3416ef..1eb9da61b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ - Removed `COO_Matrix` class. - Added portable `Vector` class and policy-based memory utilities. - Add new `LinearSolver` interface for linear solvers. +- Added new `Rosenbrock` integrator. ## v0.1 From c4ae3de3b1701d15f476052b92954bc7bdf524da Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Thu, 9 Jul 2026 13:50:41 -0400 Subject: [PATCH 31/57] [skip ci] Add Rodas5p citation --- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 3 +-- docs/CMakeLists.txt | 1 + docs/citations.bib | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 docs/citations.bib diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp index 25cfe3df8..a095a19dd 100644 --- a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -39,9 +39,8 @@ namespace Integrator } /** - * @brief + * @brief A 5th order Rosenbrock method suitable for solving parabolic PDEs. Created in \cite{shortauthor}. * - * @todo Add paper citation. * */ template diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 911ed19ea..38a7a9eba 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -10,6 +10,7 @@ if(${DOXYGEN_FOUND}) set(DOXYGEN_INTERACTIVE_SVG YES) set(DOXYGEN_DISTRIBUTE_GROUP_DOC YES) set(DOXYGEN_USE_MATHJAX YES) + set(DOXYGEN_CITE_BIB_FILES ${CMAKE_SOURCE_DIR}/docs/citations.bib) doxygen_add_docs(GridKitDocs ${CMAKE_SOURCE_DIR}/GridKit ${CMAKE_SOURCE_DIR}/README.md) endif() diff --git a/docs/citations.bib b/docs/citations.bib new file mode 100644 index 000000000..739cd0a19 --- /dev/null +++ b/docs/citations.bib @@ -0,0 +1,10 @@ +@article{steinebach2023construction, + title={Construction of Rosenbrock--Wanner method Rodas5P and numerical benchmarks within the Julia Differential Equations package}, + author={Steinebach, Gerd}, + journal={BIT Numerical Mathematics}, + volume={63}, + number={2}, + pages={27}, + year={2023}, + publisher={Springer} +} From c327f84bdaab436577254dec07b7eb6f9bf51785 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Thu, 9 Jul 2026 14:03:06 -0400 Subject: [PATCH 32/57] Update naming conventions --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 316 ++++++++--------- GridKit/Solver/Dynamic/Rosenbrock.hpp | 110 +++--- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 328 +++++++++--------- .../Solver/Dynamic/RosenbrockTests.hpp | 6 +- .../Solver/Dynamic/runRosenbrockTests.cpp | 2 +- 5 files changed, 381 insertions(+), 381 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 8fc00447c..4091953f3 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -32,18 +32,18 @@ namespace Integrator * */ template - std::string Rosenbrock::StepInfo::csv_report() const + std::string Rosenbrock::StepInfo::csvReport() const { std::stringstream out; out << std::scientific << std::setprecision(20) - << sim_time << ',' - << step_size << ',' - << next_step_size << ',' - << err_est << ',' - << step_no << ',' - << skip_lu << ',' - << skip_f << ',' - << accepted; + << sim_time_ << ',' + << step_size_ << ',' + << next_step_size_ << ',' + << err_est_ << ',' + << step_no_ << ',' + << skip_lu_ << ',' + << skip_f_ << ',' + << accepted_; return out.str(); } @@ -59,14 +59,14 @@ namespace Integrator { std::stringstream out; out << std::scientific << std::setprecision(20) - << "Simulation Time: " << sim_time << '\n' - << "Step Size: " << step_size << '\n' - << "Next Step Size: " << next_step_size << '\n' - << "Error Estimate: " << err_est << '\n' - << "Step Number: " << step_no << '\n' - << "Skip LU: " << skip_lu << '\n' - << "Skip F: " << skip_f << '\n' - << "Accepted: " << accepted; + << "Simulation Time: " << sim_time_ << '\n' + << "Step Size: " << step_size_ << '\n' + << "Next Step Size: " << next_step_size_ << '\n' + << "Error Estimate: " << err_est_ << '\n' + << "Step Number: " << step_no_ << '\n' + << "Skip LU: " << skip_lu_ << '\n' + << "Skip F: " << skip_f_ << '\n' + << "Accepted: " << accepted_; return out.str(); } @@ -81,15 +81,15 @@ namespace Integrator std::string Rosenbrock::Stats::report() const { std::stringstream out; - out << "Rejections: " << rejections.size() - << "\nSteps: " << num_steps - << "\nSkip LU Steps: " << skip_lu_steps.size() - << "\nMin Step: " << min_step - << "\nMax Step: " << max_step - << "\nRHS Function Evals: " << f_evals - << "\nRHS Function Skipped: " << f_skipped - << "\nJacobian Evals: " << jac_evals - << "\nLinear Solves: " << decomp_solves; + out << "Rejections: " << rejections_.size() + << "\nSteps: " << num_steps_ + << "\nSkip LU Steps: " << skip_lu_steps_.size() + << "\nMin Step: " << min_step_ + << "\nMax Step: " << max_step_ + << "\nRHS Function Evals: " << f_evals_ + << "\nRHS Function Skipped: " << f_skipped_ + << "\nJacobian Evals: " << jac_evals_ + << "\nLinear Solves: " << decomp_solves_; return out.str(); } @@ -105,17 +105,17 @@ namespace Integrator template typename Rosenbrock::Stats& Rosenbrock::Stats::operator+=(const Stats& other) { - rejections.insert(rejections.end(), other.rejections.begin(), other.rejections.end()); - skip_lu_steps.insert(skip_lu_steps.end(), other.skip_lu_steps.begin(), other.skip_lu_steps.end()); + rejections_.insert(rejections_.end(), other.rejections_.begin(), other.rejections_.end()); + skip_lu_steps_.insert(skip_lu_steps_.end(), other.skip_lu_steps_.begin(), other.skip_lu_steps_.end()); - num_steps += other.num_steps; - f_evals += other.f_evals; - f_skipped += other.f_skipped; - jac_evals += other.jac_evals; - decomp_solves += other.decomp_solves; + num_steps_ += other.num_steps_; + f_evals_ += other.f_evals_; + f_skipped_ += other.f_skipped_; + jac_evals_ += other.jac_evals_; + decomp_solves_ += other.decomp_solves_; - min_step = std::min(min_step, other.min_step); - max_step = std::max(max_step, other.max_step); + min_step_ = std::min(min_step_, other.min_step_); + max_step_ = std::max(max_step_, other.max_step_); return *this; } @@ -134,9 +134,9 @@ namespace Integrator * @param stage The stage being checked. */ template - constexpr bool Rosenbrock::Tableau::can_reuse_asum(size_t stage) const + constexpr bool Rosenbrock::Tableau::canReuseAsum(size_t stage) const { - assert(stage < num_stages); + assert(stage < num_stages_); if (stage == 0) return false; @@ -166,14 +166,14 @@ namespace Integrator * */ template - constexpr bool Rosenbrock::Tableau::can_reuse_asum_for_out() const + constexpr bool Rosenbrock::Tableau::canReuseAsumForOut() const { - if (num_stages == 1) + if (num_stages_ == 1) return false; - for (size_t j = 0; j < num_stages - 1; j++) + for (size_t j = 0; j < num_stages_ - 1; j++) { - if (getA(num_stages - 1, j) != m[j]) + if (getA(num_stages_ - 1, j) != m_[j]) { return false; } @@ -193,18 +193,18 @@ namespace Integrator * are included in this tableau. */ template - constexpr std::optional Rosenbrock::Tableau::error_estimator_stage() const + constexpr std::optional Rosenbrock::Tableau::errorEstimatorStage() const { - assert(e); + assert(e_); std::optional re; - for (size_t j = 0; j < num_stages; j++) + for (size_t j = 0; j < num_stages_; j++) { - if (e[j] == 1.0 && !re) + if (e_[j] == 1.0 && !re) { re = j; } - else if (e[j] != 0.0) + else if (e_[j] != 0.0) { return {}; } @@ -282,9 +282,9 @@ namespace Integrator BUBBLE_FAIL(workspace_.dFdt_->allocate(memspace_)); BUBBLE_FAIL(workspace_.mass_->allocate(memspace_)); - if (tab_.e) + if (tab_.e_) { - std::optional err_est_stage = tab_.error_estimator_stage(); + std::optional err_est_stage = tab_.errorEstimatorStage(); if (!err_est_stage) { workspace_.err_est_ = std::make_unique(size); @@ -292,18 +292,18 @@ namespace Integrator } } - workspace_.stages_ = std::make_unique[]>(tab_.num_stages); - for (size_t i = 0; i < tab_.num_stages; i++) + workspace_.stages_ = std::make_unique[]>(tab_.num_stages_); + for (size_t i = 0; i < tab_.num_stages_; i++) { workspace_.stages_[i] = std::make_unique(size); BUBBLE_FAIL(workspace_.stages_[i]->allocate(memspace_)); workspace_.stages_[i]->setToZero(memspace_); } - if (tab_.order > 2) + if (tab_.order_ > 2) { - dense_coeff_ = std::make_unique[]>(tab_.order - 2); - for (size_t i = 0; i < static_cast(tab_.order - 2); i++) + dense_coeff_ = std::make_unique[]>(tab_.order_ - 2); + for (size_t i = 0; i < static_cast(tab_.order_ - 2); i++) { dense_coeff_[i] = std::make_unique(size); BUBBLE_FAIL(dense_coeff_[i]->allocate(memspace_)); @@ -316,7 +316,7 @@ namespace Integrator } /** - * @brief Initializes the simulation. Must be called before \ref integrate() or \ref time_step(). Must also be called after + * @brief Initializes the simulation. Must be called before \ref integrate() or \ref timeStep(). Must also be called after * discrete events. * * - Sets the simulation time to `t0` and copies the initial condition from \ref model_. @@ -372,7 +372,7 @@ namespace Integrator * @brief Simulate the given model, producing output at the given output times. * * Implements a simple time stepping algorithm and facilitates output. - * 1. Calls \ref time_step() to move the simulation forward by \ref step_size_ (starting at `params.starting_step`). + * 1. Calls \ref timeStep() to move the simulation forward by \ref step_size_ (starting at `params.starting_step`). * 2. Consults the `step_controller` to see if the step is accepted, and what the next \ref step_size_ should be. * 3. If accepted, advance simulation by adding \ref step_size_ to \ref current_time_ using Kahan summation and shifting * \ref y_new_ -> \ref y_cur_ -> \ref y_prev_. @@ -422,7 +422,7 @@ namespace Integrator skip_f_ = false; bool prev_accept = true; - step_size_ = params.starting_step; + step_size_ = params.starting_step_; double next_step_size; @@ -433,9 +433,9 @@ namespace Integrator // Generate output for each output time for (double out_time : out_times) { - while (current_time_ < out_time && stats_.num_steps < params.max_steps) + while (current_time_ < out_time && stats_.num_steps_ < params.max_steps_) { - BUBBLE_FAIL(time_step(current_time_, step_size_)); + BUBBLE_FAIL(timeStep(current_time_, step_size_)); double err = 0; @@ -448,18 +448,18 @@ namespace Integrator return -1; } - State& err_vec = error_estimate(); + State& err_vec = errorEstimate(); err = err_norm_->errorNorm(err_vec, *y_new_, *y_cur_, vector_handler_, memspace_); } StepControl next_step = step_controller.nextStep(err, StepControl{ - .accept = prev_accept, - .step_size = step_size_, + .accept_ = prev_accept, + .step_size_ = step_size_, }, - tab_.order); - prev_accept = next_step.accept; - next_step_size = next_step.step_size; + tab_.order_); + prev_accept = next_step.accept_; + next_step_size = next_step.step_size_; if (prev_accept) { @@ -476,22 +476,22 @@ namespace Integrator skip_f_ = false; dense_coefficients_valid_ = false; - stats_.num_steps++; + stats_.num_steps_++; if (skip_lu_) { - stats_.skip_lu_steps.push_back(StepInfo{ - .sim_time = current_time_, - .step_size = step_size_, - .next_step_size = next_step_size, - .err_est = err, - .step_no = stats_.num_steps, - .skip_lu = skip_lu_, - .skip_f = skip_f_, - .accepted = prev_accept, + stats_.skip_lu_steps_.push_back(StepInfo{ + .sim_time_ = current_time_, + .step_size_ = step_size_, + .next_step_size_ = next_step_size, + .err_est_ = err, + .step_no_ = stats_.num_steps_, + .skip_lu_ = skip_lu_, + .skip_f_ = skip_f_, + .accepted_ = prev_accept, }); } - stats_.min_step = std::min(stats_.min_step, step_size_); - stats_.max_step = std::max(stats_.max_step, step_size_); + stats_.min_step_ = std::min(stats_.min_step_, step_size_); + stats_.max_step_ = std::max(stats_.max_step_, step_size_); std::swap(y_prev_, y_cur_); std::swap(y_cur_, y_new_); @@ -500,15 +500,15 @@ namespace Integrator { skip_f_ = true; - stats_.rejections.push_back(StepInfo{ - .sim_time = current_time_, - .step_size = step_size_, - .next_step_size = next_step_size, - .err_est = err, - .step_no = stats_.num_steps, - .skip_lu = skip_lu_, - .skip_f = skip_f_, - .accepted = prev_accept, + stats_.rejections_.push_back(StepInfo{ + .sim_time_ = current_time_, + .step_size_ = step_size_, + .next_step_size_ = next_step_size, + .err_est_ = err, + .step_no_ = stats_.num_steps_, + .skip_lu_ = skip_lu_, + .skip_f_ = skip_f_, + .accepted_ = prev_accept, }); } @@ -516,7 +516,7 @@ namespace Integrator // instead keep the step size the same and use time-delay Jacobian. // TODO: configure upper bound here double step_gain = next_step_size / step_size_; - if (params.skip_lu && step_gain >= 1 && step_gain <= 1.2) + if (params.skip_lu_ && step_gain >= 1 && step_gain <= 1.2) { skip_lu_ = true; } @@ -534,14 +534,14 @@ namespace Integrator model_->updateTime(current_time_, 0.0); (*step_cb)(StepInfo{ - .sim_time = current_time_, - .step_size = step_size_, - .next_step_size = next_step_size, - .err_est = err, - .step_no = stats_.num_steps, - .skip_lu = skip_lu_, - .skip_f = skip_f_, - .accepted = prev_accept, + .sim_time_ = current_time_, + .step_size_ = step_size_, + .next_step_size_ = next_step_size, + .err_est_ = err, + .step_no_ = stats_.num_steps_, + .skip_lu_ = skip_lu_, + .skip_f_ = skip_f_, + .accepted_ = prev_accept, }); } } @@ -559,11 +559,11 @@ namespace Integrator { if (!dense_coefficients_valid_) { - BUBBLE_FAIL(calc_dense_coeff()); + BUBBLE_FAIL(calcDenseCoeff()); dense_coefficients_valid_ = true; } - BUBBLE_FAIL(interp_dense(theta)); + BUBBLE_FAIL(interpDense(theta)); } else { @@ -605,7 +605,7 @@ namespace Integrator * * where the coefficients \f(\gamma, \alpha_i, a_{ij}, c_{ij}, m_j\f) come from the tableau, and the mass matrix \f(M\f) and Jacobian \f(J\f) come from the model. * - * This method uses some state which is maintained between calls for future calls to `time_step()` and to communicate with \ref integrate(): + * This method uses some state which is maintained between calls for future calls to `timeStep()` and to communicate with \ref integrate(): * - \ref y_cur_ is used as \f(y_0\f) and \ref y_new_ is used as \f(y_1\f) * - \ref asum_ is used as \f(y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\f) for every stage except the first. Often, \f(a_{ij} = a_{i-1,j}\f), so this variable * can be re-used between stages to save computation. Similarly, often \f(a_{ij} = m_j\f), so this variable can be re-used for computing \ref y_new_. @@ -615,13 +615,13 @@ namespace Integrator * for the stage value \f(u_i\f). * - \ref RHS_first_stage_ is used as a special \ref RHS_ for the first stage, since it may need to be saved for the next step for the \ref skip_f_ flag. * Since \f(\alpha_1 = 0\f) always, this will have a value of \f(f \left(t_0, y_0\right)\f). - * - \ref stages_ stores all of the stages \f(u_i\f). These stages are necessary to be used in future calls to \ref error_estimate() and \ref calc_dense_coeff(). + * - \ref stages_ stores all of the stages \f(u_i\f). These stages are necessary to be used in future calls to \ref errorEstimate() and \ref calcDenseCoeff(). * - \ref skip_lu_ is used as a flag set by \ref integrate() to indicate that it is appropriate to use a time-delay Jacobian by re-using the factorization * of the last step. - * - \ref skip_f_ is used as a flag set by \ref integrate() to indicate that \f(t_0, y_0\f) have not changed since the last time `time_step()` was called, + * - \ref skip_f_ is used as a flag set by \ref integrate() to indicate that \f(t_0, y_0\f) have not changed since the last time `timeStep()` was called, * so \ref RHS_first_stage_ can be re-used from the previous step. This will only happen when a step was rejected, so \ref skip_lu_ should always be false, * and the entire first stage can't be re-used. - * - \ref jacobian_analyzed_ keeps track of whether the Jacobian has been factored in a previous call to `time_step()`. If so, then it can be re-factored + * - \ref jacobian_analyzed_ keeps track of whether the Jacobian has been factored in a previous call to `timeStep()`. If so, then it can be re-factored * in a faster way. The first factor must be done on actual data, so it cannot be performed pre-simulation. * * @pre Must call \ref initializeSimulation() beforehand. @@ -645,7 +645,7 @@ namespace Integrator * @return An error code, with 0 as success. */ template - int Rosenbrock::time_step(double t0, double dt) + int Rosenbrock::timeStep(double t0, double dt) { // A flag to keep track of if y0 (stored in y_cur_) has been copied in to the model already, to avoid double-copying // for evaluating the Jacobian and residual on stage 1 (both evaluated at y0). @@ -654,14 +654,14 @@ namespace Integrator // Form the left-hand side of the system. This is constant between stages. // Can sometimes be skipped if the method allows for time-delay Jacobians (such as w-methods). [[likely]] - if (!tab_.is_w || !skip_lu_) + if (!tab_.is_w_ || !skip_lu_) { BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); y0_copied = true; // GridKit, like IDA, expects to evaluate the Jacobian J = df/dy + alpha * df/dy', // so we need a negative here since df/dy' = M. - model_->updateTime(t0, -1.0 / (dt * tab_.gamma)); + model_->updateTime(t0, -1.0 / (dt * tab_.gamma_)); BUBBLE_FAIL(model_->evaluateJacobian()); GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); @@ -684,14 +684,14 @@ namespace Integrator workspace_.jacobian_analyzed_ = true; } - stats_.jac_evals++; + stats_.jac_evals_++; } // First stage [[unlikely]] if (skip_f_) { - stats_.f_skipped++; + stats_.f_skipped_++; } else { @@ -706,22 +706,22 @@ namespace Integrator BUBBLE_FAIL(workspace_.RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); vector_handler_.scal(-1, workspace_.RHS_first_stage_.get(), memspace_); - stats_.f_evals++; + stats_.f_evals_++; } resolve_rhs_->setData(workspace_.RHS_first_stage_->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); resolve_lhs_->setData(workspace_.stages_[0]->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); BUBBLE_FAIL(lin_solver_.solve(resolve_rhs_.get(), resolve_lhs_.get())); - stats_.decomp_solves++; + stats_.decomp_solves_++; // Rest of stages - for (size_t i = 1; i < tab_.num_stages; i++) + for (size_t i = 1; i < tab_.num_stages_; i++) { // Calculate asum // We can sometimes reuse asum from the previous stage - if (i > 1 && tab_.can_reuse_asum(i)) + if (i > 1 && tab_.canReuseAsum(i)) { - if (tab_.A[tab_.num_stages * i + i - 1] != 0.0) - vector_handler_.axpy(tab_.A[tab_.num_stages * i + i - 1], workspace_.stages_[i - 1].get(), workspace_.asum_.get(), memspace_); + if (tab_.A_[tab_.num_stages_ * i + i - 1] != 0.0) + vector_handler_.axpy(tab_.A_[tab_.num_stages_ * i + i - 1], workspace_.stages_[i - 1].get(), workspace_.asum_.get(), memspace_); } else { @@ -729,8 +729,8 @@ namespace Integrator for (size_t j = 0; j < i; j++) { - if (tab_.A[tab_.num_stages * i + j] != 0.0) - vector_handler_.axpy(tab_.A[tab_.num_stages * i + j], workspace_.stages_[j].get(), workspace_.asum_.get(), memspace_); + if (tab_.A_[tab_.num_stages_ * i + j] != 0.0) + vector_handler_.axpy(tab_.A_[tab_.num_stages_ * i + j], workspace_.stages_[j].get(), workspace_.asum_.get(), memspace_); } } @@ -739,15 +739,15 @@ namespace Integrator BUBBLE_FAIL(workspace_.csum_->setToZero(memspace_)); for (size_t j = 0; j < i; j++) { - if (tab_.C[i * tab_.num_stages + j] != 0.0) + if (tab_.C_[i * tab_.num_stages_ + j] != 0.0) { - vector_handler_.axpy(tab_.C[i * tab_.num_stages + j] / dt, workspace_.stages_[j].get(), workspace_.csum_.get(), memspace_); + vector_handler_.axpy(tab_.C_[i * tab_.num_stages_ + j] / dt, workspace_.stages_[j].get(), workspace_.csum_.get(), memspace_); } } // TODO: non-autonomous model BUBBLE_FAIL(workspace_.asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(t0 + tab_.alpha_sum[i] * dt, 0.0); + model_->updateTime(t0 + tab_.alpha_sum_[i] * dt, 0.0); BUBBLE_FAIL(model_->evaluateResidual()); workspace_.RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); @@ -759,27 +759,27 @@ namespace Integrator resolve_rhs_->setData(workspace_.RHS_->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); resolve_lhs_->setData(workspace_.stages_[i]->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); BUBBLE_FAIL(lin_solver_.solve(resolve_rhs_.get(), resolve_lhs_.get())); - stats_.f_evals++; - stats_.decomp_solves++; + stats_.f_evals_++; + stats_.decomp_solves_++; } // Compute the solution at time t + dt // It happens often where the solution is just asum of the last stage // plus some multiple of the last stage. In that case we can avoid a matmul - if (tab_.can_reuse_asum_for_out()) + if (tab_.canReuseAsumForOut()) { std::swap(workspace_.asum_, y_new_); - vector_handler_.axpy(tab_.m[tab_.num_stages - 1], workspace_.stages_[tab_.num_stages - 1].get(), y_new_.get(), memspace_); + vector_handler_.axpy(tab_.m_[tab_.num_stages_ - 1], workspace_.stages_[tab_.num_stages_ - 1].get(), y_new_.get(), memspace_); } else { BUBBLE_FAIL(y_new_->copyFromExternal(*y_cur_, memspace_, memspace_)); - for (size_t j = 0; j < tab_.num_stages; j++) + for (size_t j = 0; j < tab_.num_stages_; j++) { - if (tab_.m[j] != 0.0) + if (tab_.m_[j] != 0.0) { - vector_handler_.axpy(tab_.m[j], workspace_.stages_[j].get(), y_new_.get(), memspace_); + vector_handler_.axpy(tab_.m_[j], workspace_.stages_[j].get(), y_new_.get(), memspace_); } } } @@ -788,18 +788,18 @@ namespace Integrator } /** - * @brief Calculates an estimation of the error produced by the last call to \ref time_step() which can be used as the + * @brief Calculates an estimation of the error produced by the last call to \ref timeStep() which can be used as the * `err` argument for \ref ErrorNorm::errorNorm(). * * Calculate the embedded error as * * \f[\hat{e} = \sum_{j = 1}^s e_j u_j,\f] * - * where \f(e_j\f) are tableau coefficients and \f(u_j\f) are stages computed by \ref time_step(). It happens often that + * where \f(e_j\f) are tableau coefficients and \f(u_j\f) are stages computed by \ref timeStep(). It happens often that * \f(e_j = 0\f) for all but one stage, where \f(e_j = 1\f). In that case, the stage itself is used as the error estimate, * and extra calculation can be avoided. For this reason, a reference to the estimate is returned to avoid an unnecessary copy. * - * @pre Must call \ref time_step(), and tableau must have coefficients for an embedded error estimator. + * @pre Must call \ref timeStep(), and tableau must have coefficients for an embedded error estimator. * @note This method can fail. * * @todo This function is fallible, but the return type makes it difficult to return an error code. Right now it will throw an @@ -808,11 +808,11 @@ namespace Integrator * @return A reference to the estimated error. */ template - Rosenbrock::State& Rosenbrock::error_estimate() const + Rosenbrock::State& Rosenbrock::errorEstimate() const { // Test to see if the tableau allows us to use a stage as the error estimate, // avoiding extra computation. - std::optional err_stage = tab_.error_estimator_stage(); + std::optional err_stage = tab_.errorEstimatorStage(); if (err_stage) { @@ -828,12 +828,12 @@ namespace Integrator throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); } - vector_handler_.scal(tab_.e[0], workspace_.err_est_.get(), memspace_); - for (size_t j = 1; j < tab_.num_stages; j++) + vector_handler_.scal(tab_.e_[0], workspace_.err_est_.get(), memspace_); + for (size_t j = 1; j < tab_.num_stages_; j++) { - if (tab_.e[j] != 0.0) + if (tab_.e_[j] != 0.0) { - vector_handler_.axpy(tab_.e[j], workspace_.stages_[j].get(), workspace_.err_est_.get(), memspace_); + vector_handler_.axpy(tab_.e_[j], workspace_.stages_[j].get(), workspace_.err_est_.get(), memspace_); } } @@ -842,32 +842,32 @@ namespace Integrator } /** - * @brief Calculate the interpolation nodes used by \ref interp_dense() based on \ref stages_ computed by - * the last call to \ref time_step(). Nodes are stored in \ref dense_coeff_. Only needs to be called once, - * but can be invalidated by future calls to \ref time_step(). \ref integrate() keeps track of \ref dense_coefficients_valid_, + * @brief Calculate the interpolation nodes used by \ref interpDense() based on \ref stages_ computed by + * the last call to \ref timeStep(). Nodes are stored in \ref dense_coeff_. Only needs to be called once, + * but can be invalidated by future calls to \ref timeStep(). \ref integrate() keeps track of \ref dense_coefficients_valid_, * which tells you if this function is needed to be called. * - * @pre Must call \ref time_step() first. + * @pre Must call \ref timeStep() first. * * @note This method can fail. * * @return An error code, with 0 as success. */ template - int Rosenbrock::calc_dense_coeff() + int Rosenbrock::calcDenseCoeff() { - if (tab_.order > 2) + if (tab_.order_ > 2) { - for (size_t j = 0; j < static_cast(tab_.order - 2); j++) + for (size_t j = 0; j < static_cast(tab_.order_ - 2); j++) { BUBBLE_FAIL(dense_coeff_[j]->setToZero(memspace_)); } - for (size_t i = 0; i < tab_.num_stages; i++) + for (size_t i = 0; i < tab_.num_stages_; i++) { - for (size_t j = 0; j < static_cast(tab_.order - 2); j++) + for (size_t j = 0; j < static_cast(tab_.order_ - 2); j++) { - vector_handler_.axpy(tab_.H[j * tab_.num_stages + i], workspace_.stages_[i].get(), dense_coeff_[j].get(), memspace_); + vector_handler_.axpy(tab_.H_[j * tab_.num_stages_ + i], workspace_.stages_[i].get(), dense_coeff_[j].get(), memspace_); } } } @@ -878,7 +878,7 @@ namespace Integrator /** * @brief Calculate an interpolated state at \f(\theta = \frac{t - t_0}{h}\f) between the initial state * \f(y_0\f) and final state \f(y_1\f) of the last step taken using dense interpolation nodes calculated - * by \ref calc_dense_coeff(). + * by \ref calcDenseCoeff(). * * For a valid interpolation of appropriate order, \f(\theta\f) must be in \f([0, 1]\f), although values * beyond 1 can be used for extrapolation if desired. @@ -890,10 +890,10 @@ namespace Integrator * * \f[y(\theta) = (1 - \theta) y_0 + \theta \left(y_1 + (1 - \theta) \sum_{i = 1}^{p-2} \theta^{i-1} \hat{y}_i\right),\f] * - * where \f(\hat{y}_i\f) are the dense interpolation nodes calculated in \ref calc_dense_coeff() and \f(p\f) is the order of the method. + * where \f(\hat{y}_i\f) are the dense interpolation nodes calculated in \ref calcDenseCoeff() and \f(p\f) is the order of the method. * This calculation is carried out using synthetic division. * - * @pre Must call \ref calc_dense_coeff() first. + * @pre Must call \ref calcDenseCoeff() first. * * @note This method can fail. * @@ -904,16 +904,16 @@ namespace Integrator * @return An error code, with 0 as success. */ template - int Rosenbrock::interp_dense(double theta) + int Rosenbrock::interpDense(double theta) { - if (tab_.order > 2) + if (tab_.order_ > 2) { - BUBBLE_FAIL(y_interp_->copyFromExternal(*dense_coeff_[tab_.order - 3], memspace_, memspace_)); + BUBBLE_FAIL(y_interp_->copyFromExternal(*dense_coeff_[tab_.order_ - 3], memspace_, memspace_)); - for (size_t i = 1; i < static_cast(tab_.order - 2); i++) + for (size_t i = 1; i < static_cast(tab_.order_ - 2); i++) { vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(1.0, dense_coeff_[tab_.order - 3 - i].get(), y_interp_.get(), memspace_); + vector_handler_.axpy(1.0, dense_coeff_[tab_.order_ - 3 - i].get(), y_interp_.get(), memspace_); } // TODO: This scal can be removed and absorbed into the next axpy, except that it currently isn't possible to put a scalar @@ -942,10 +942,10 @@ namespace Integrator { StepControl next_step = prev_step; - double h_mult = std::min(params_.facmax, std::max(params_.facscale * std::pow(err, -1.0 / method_order), params_.facmin)); + double h_mult = std::min(params_.fac_max_, std::max(params_.fac_scale_ * std::pow(err, -1.0 / method_order), params_.fac_min_)); - next_step.accept = err <= 1; - next_step.step_size *= h_mult; + next_step.accept_ = err <= 1; + next_step.step_size_ *= h_mult; return next_step; } @@ -957,8 +957,8 @@ namespace Integrator StepControl FixedStep::nextStep([[maybe_unused]] double err, StepControl prev_step, [[maybe_unused]] uint8_t method_order) { return StepControl{ - .accept = true, - .step_size = prev_step.step_size, + .accept_ = true, + .step_size_ = prev_step.step_size_, }; } @@ -975,7 +975,7 @@ namespace Integrator * @param yprev \f(y_0\f) in the above formula. * @param handler The handler to be used for performing linear algebra operations. * @param memspace The memory space to be used for performing linear lagebra operations. - * @see `Rosenbrock::error_estimate()` + * @see `Rosenbrock::errorEstimate()` */ template double InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const @@ -999,8 +999,8 @@ namespace Integrator // TODO: This scal shouldn't be necessary, but axpy doesn't support scaling the y parameter. In the future, // the scaling should be able to be put on the next axpy. - handler.scal(params_.rtol, workspace_.scale_.get(), memspace); - handler.axpy(1.0, params_.atol.get(), workspace_.scale_.get(), memspace); + handler.scal(params_.rel_tol_, workspace_.scale_.get(), memspace); + handler.axpy(1.0, params_.abs_tol_.get(), workspace_.scale_.get(), memspace); handler.diagSolve(workspace_.scale_.get(), workspace_.out_.get(), memspace); return handler.amax(workspace_.out_.get(), memspace); diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index be4e43cb8..9daa86d35 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -32,12 +32,12 @@ namespace Integrator * the next state and re-step with the new `step_size`. * */ - bool accept; + bool accept_; /** * @brief The step size the next step should take. * */ - double step_size; + double step_size_; }; /** @@ -116,44 +116,44 @@ namespace Integrator * @brief The simulation time at the beginning of the step * */ - double sim_time; + double sim_time_; /** * @brief The size of the step. * */ - double step_size; + double step_size_; /** * @brief The size of the next step, as governed by the current `StepController` in use. * */ - double next_step_size; + double next_step_size_; /** * @brief The estimated error made by the step, as calculated by the current `ErrorNorm` in use. * */ - double err_est; + double err_est_; /** * @brief The step number, starting at 1. * */ - size_t step_no; + size_t step_no_; /** * @brief Whether or not the integrator decided to skip computing the decomposition of the Jacobian on this step. * */ - bool skip_lu; + bool skip_lu_; /** * @brief Whether or not the integrator decided to skip evaluating the residual on the first stage on this step. * */ - bool skip_f; + bool skip_f_; /** * @brief Whether or not this step was accepted by the `StepController` in use. * */ - bool accepted; + bool accepted_; - std::string csv_report() const; + std::string csvReport() const; std::string report() const; }; @@ -167,47 +167,47 @@ namespace Integrator * @brief Information of each step which has been rejected. * */ - std::vector rejections; + std::vector rejections_; /** * @brief Information of each step which the integrator decided to skip re-factoring the Jacobian. * */ - std::vector skip_lu_steps; + std::vector skip_lu_steps_; /** * @brief How many steps the integrator has taken. * */ - size_t num_steps = 0; + size_t num_steps_ = 0; /** * @brief Number of model residual function evaluations. * */ - size_t f_evals = 0; + size_t f_evals_ = 0; /** * @brief Number of model residual function evaluations which have been skipped by the integrator. * */ - size_t f_skipped = 0; + size_t f_skipped_ = 0; /** * @brief Number of model Jacobian evaluations. * */ - size_t jac_evals = 0; + size_t jac_evals_ = 0; /** * @brief Number of linear solves against the model Jacobian. * */ - size_t decomp_solves = 0; + size_t decomp_solves_ = 0; /** * @brief Minimum step size. * */ - double min_step = INFINITY; + double min_step_ = INFINITY; /** * @brief Maximum step size. * */ - double max_step = 0; + double max_step_ = 0; std::string report() const; Stats& operator+=(const Stats& other); @@ -224,13 +224,13 @@ namespace Integrator * * @todo Consider adding a starting step size selector to select this automatically. */ - double starting_step = 1e-5; + double starting_step_ = 1e-5; /** * @brief The maximum number of steps the integrator should take. If the integrator has not reached the final time before * taking this many steps, then integration is stopped. For more details, see `integrate()`. * */ - size_t max_steps = 2000; + size_t max_steps_ = 2000; /** * @brief Whether or not the integrator should attempt to skip Jacobian decompositions. * @@ -238,7 +238,7 @@ namespace Integrator * the time taken to compute each step. However, the overall number of steps taken will increase. * */ - bool skip_lu = false; + bool skip_lu_ = false; }; /** @@ -253,49 +253,49 @@ namespace Integrator * @brief The number of stages used by the method. Each stage requires one model residual evaluation. * */ - size_t num_stages; + size_t num_stages_; /** * @brief The coefficient along the diagonal of the Gamma matrix. * */ - RealT gamma; + RealT gamma_; /** * @brief A vector of sums of rows of the alpha matrix. These are the classic * Runge-Kutta 'c' coefficients, or abscissae. The size of this vector * should be equal to `num_stages`. * */ - std::unique_ptr alpha_sum; + std::unique_ptr alpha_sum_; /** * @brief A vector of sums of rows of the Gamma matrix. The size of this vector * should be equal to `num_stages`. * */ - std::unique_ptr gamma_sum; + std::unique_ptr gamma_sum_; /** * @brief A vector of weights for constructing the final solution from the stages. * The size of this vector should be equal to `num_stages`. * */ - std::unique_ptr m; + std::unique_ptr m_; /** * @brief OPTIONAL vector of coefficients for the embedded error method. If it exists, * the size of this vector should be equal to `num_stages`. * */ - std::unique_ptr e; + std::unique_ptr e_; /** * @brief The transformed A coefficient matrix. Strictly lower triangular and stored in dense row-major form. * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. * */ - std::unique_ptr A; + std::unique_ptr A_; /** * @brief The transformed C coefficient matrix. Strictly lower triangular and stored in dense row-major form. * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. * */ - std::unique_ptr C; + std::unique_ptr C_; /** * @brief OPTIONAL matrix of dense coefficients. Defines how the stages should be transformed into interpolant * nodes for computing dense output. The interpolating polynomial has an order one less than the order of @@ -303,7 +303,7 @@ namespace Integrator * `order` - 2 by `num_stages` large. * */ - std::unique_ptr H; + std::unique_ptr H_; /** * @brief What ODE order these coefficients satisfy. If `is_dae` is true, then the coefficients must additionally satisfy * DAE conditions up to this order. If `is_w` is true, then the coefficients must additionally satisfy ROW conditions @@ -311,33 +311,33 @@ namespace Integrator * */ - uint8_t order; + uint8_t order_; /** * @brief Whether or not these coefficients are appropriate to use in a Rosenbrock-Krylov (ROK) solver. * */ - bool is_krylov; + bool is_krylov_; /** * @brief Whether or not these coefficients satisfy Rosenbrock-W (ROW) order conditions up to `order`. * The integrator may take advantage of this fact by e.g. using time-delay Jacobians to speed up computation. * */ - bool is_w; + bool is_w_; /** * @brief Whether or not these coefficients satisfy DAE order conditions up to `order`. If this is not true, * these coefficients should not be used to solve models with algebraic conditions (indicated by a * `Model::Evaluator::tag_` value of 0). * */ - bool is_dae; + bool is_dae_; /** * @brief Whether or not this tableau contains an embedded error estimator method. * */ - constexpr bool has_embedded() const + constexpr bool hasEmbedded() const { - return static_cast(e); + return static_cast(e_); } /** @@ -346,7 +346,7 @@ namespace Integrator */ constexpr bool hasDenseOutput() const { - return static_cast(H); + return static_cast(H_); } /** @@ -355,14 +355,14 @@ namespace Integrator */ constexpr RealT getA(size_t row, size_t col) const { - return A[row * num_stages + col]; + return A_[row * num_stages_ + col]; } - constexpr bool can_reuse_asum(size_t stage) const; - constexpr bool can_reuse_asum_for_out() const; - constexpr std::optional error_estimator_stage() const; + constexpr bool canReuseAsum(size_t stage) const; + constexpr bool canReuseAsumForOut() const; + constexpr std::optional errorEstimatorStage() const; - static Tableau lin_implicit_euler(); + static Tableau linImplicitEuler(); static Tableau rodas5p(); }; @@ -414,7 +414,7 @@ namespace Integrator */ GridKit::Model::Evaluator* model_; /** - * @brief The linear solver to be used during integration in \link time_step() \endlink. + * @brief The linear solver to be used during integration in \link timeStep() \endlink. * */ ReSolve::SystemSolver& lin_solver_; @@ -586,22 +586,22 @@ namespace Integrator /** * @brief Dense output interpolation nodes. Used to generate output states in-between steps. * - * @see \link calc_dense_coeff() \endlink, \link interp_dense() \endlink + * @see \link calcDenseCoeff() \endlink, \link interpDense() \endlink * */ std::unique_ptr[]> dense_coeff_; public: [[nodiscard("May fail. Check error code.")]] - int time_step(double t0, double dt); + int timeStep(double t0, double dt); - State& error_estimate() const; + State& errorEstimate() const; [[nodiscard("May fail. Check error code.")]] - int calc_dense_coeff(); + int calcDenseCoeff(); [[nodiscard("May fail. Check error code.")]] - int interp_dense(double theta); + int interpDense(double theta); }; /** @@ -625,7 +625,7 @@ namespace Integrator * @note Should be between 0 and 1. * */ - double facmin = 0.2; + double fac_min_ = 0.2; /** * @brief The maximum multiple by which the step size can be multiplied to obtain the new step size. * Decreasing this will make the integrator more conservative in selecting the step size - @@ -634,7 +634,7 @@ namespace Integrator * @note Should be greater than 1. * */ - double facmax = 5.0; + double fac_max_ = 5.0; /** * @brief A "fudge factor" introduced to decrease risk of failing a step. The larger the fudge factor, * the more likely steps will fail, but fewer steps will be taken. @@ -642,7 +642,7 @@ namespace Integrator * @note Should be between 0 and 1. * */ - double facscale = 0.9; + double fac_scale_ = 0.9; } params_; public: @@ -734,14 +734,14 @@ namespace Integrator * in each component above this tolerance. * */ - std::unique_ptr atol; + std::unique_ptr abs_tol_; /** * @brief The relative tolerance. The norm will attempt to reject any error larger in percentage of * the solution's maximum element than this. * */ - double rtol; + double rel_tol_; } params_; InfNorm(Parameters&& params) diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp index a095a19dd..a9235f14c 100644 --- a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -5,35 +5,35 @@ namespace Integrator { template - Rosenbrock::Tableau Rosenbrock::Tableau::lin_implicit_euler() + Rosenbrock::Tableau Rosenbrock::Tableau::linImplicitEuler() { constexpr size_t num_stages = 1; Tableau re = { - .num_stages = num_stages, - .gamma = 1.0, - .alpha_sum = std::make_unique_for_overwrite(num_stages), - .gamma_sum = std::make_unique_for_overwrite(num_stages), - .m = std::make_unique_for_overwrite(num_stages), - .e = {}, - .A = std::make_unique_for_overwrite(num_stages * num_stages), - .C = std::make_unique_for_overwrite(num_stages * num_stages), - .H = {}, - .order = 1, - .is_krylov = true, - .is_w = false, - .is_dae = true, + .num_stages_ = num_stages, + .gamma_ = 1.0, + .alpha_sum_ = std::make_unique_for_overwrite(num_stages), + .gamma_sum_ = std::make_unique_for_overwrite(num_stages), + .m_ = std::make_unique_for_overwrite(num_stages), + .e_ = {}, + .A_ = std::make_unique_for_overwrite(num_stages * num_stages), + .C_ = std::make_unique_for_overwrite(num_stages * num_stages), + .H_ = {}, + .order_ = 1, + .is_krylov_ = true, + .is_w_ = false, + .is_dae_ = true, }; - re.alpha_sum[0] = 0.0; + re.alpha_sum_[0] = 0.0; - re.gamma_sum[0] = 1.0; + re.gamma_sum_[0] = 1.0; - re.m[0] = 1.0; + re.m_[0] = 1.0; - re.A[0] = 0.0; + re.A_[0] = 0.0; - re.C[0] = 2.0; + re.C_[0] = 2.0; return re; } @@ -49,153 +49,153 @@ namespace Integrator constexpr size_t num_stages = 8; Tableau re = { - .num_stages = num_stages, - .gamma = 0.21193756319429014, - .alpha_sum = std::make_unique_for_overwrite(num_stages), - .gamma_sum = std::make_unique_for_overwrite(num_stages), - .m = std::make_unique_for_overwrite(num_stages), - .e = std::make_unique_for_overwrite(num_stages), - .A = std::make_unique_for_overwrite(num_stages * num_stages), - .C = std::make_unique_for_overwrite(num_stages * num_stages), - .H = std::make_unique_for_overwrite(num_stages * 3), - .order = 5, - .is_krylov = false, - .is_w = false, - .is_dae = true, + .num_stages_ = num_stages, + .gamma_ = 0.21193756319429014, + .alpha_sum_ = std::make_unique_for_overwrite(num_stages), + .gamma_sum_ = std::make_unique_for_overwrite(num_stages), + .m_ = std::make_unique_for_overwrite(num_stages), + .e_ = std::make_unique_for_overwrite(num_stages), + .A_ = std::make_unique_for_overwrite(num_stages * num_stages), + .C_ = std::make_unique_for_overwrite(num_stages * num_stages), + .H_ = std::make_unique_for_overwrite(num_stages * 3), + .order_ = 5, + .is_krylov_ = false, + .is_w_ = false, + .is_dae_ = true, }; - re.alpha_sum[0] = 0.0; - re.alpha_sum[1] = 0.6358126895828704; - re.alpha_sum[2] = 0.4095798393397535; - re.alpha_sum[3] = 0.9769306725060716; - re.alpha_sum[4] = 0.4288403609558664; - re.alpha_sum[5] = 1.0; - re.alpha_sum[6] = 1.0; - re.alpha_sum[7] = 1.0; - - re.gamma_sum[0] = 0.21193756319429014; - re.gamma_sum[1] = -0.42387512638858027; - re.gamma_sum[2] = -0.3384627126235924; - re.gamma_sum[3] = 1.8046452872882734; - re.gamma_sum[4] = 2.325825639765069; - re.gamma_sum[5] = 0.0; - re.gamma_sum[6] = 0.0; - re.gamma_sum[7] = 0.0; - - re.m[0] = -7.502846399306121; - re.m[1] = 2.561846144803919; - re.m[2] = -11.627539656261098; - re.m[3] = -0.18268767659942256; - re.m[4] = 0.030198172008377946; - re.m[5] = 1.0; - re.m[6] = 1.0; - re.m[7] = 1.0; - - re.e[0] = 0.0; - re.e[1] = 0.0; - re.e[2] = 0.0; - re.e[3] = 0.0; - re.e[4] = 0.0; - re.e[5] = 0.0; - re.e[6] = 0.0; - re.e[7] = 1.0; - - re.A[1 * 8 + 0] = 3.0; - - re.A[2 * 8 + 0] = 2.849394379747939; - re.A[2 * 8 + 1] = 0.45842242204463923; - - re.A[3 * 8 + 0] = -6.954028509809101; - re.A[3 * 8 + 1] = 2.489845061869568; - re.A[3 * 8 + 2] = -10.358996098473584; - - re.A[4 * 8 + 0] = 2.8029986275628964; - re.A[4 * 8 + 1] = 0.5072464736228206; - re.A[4 * 8 + 2] = -0.3988312541770524; - re.A[4 * 8 + 3] = -0.04721187230404641; - - re.A[5 * 8 + 0] = -7.502846399306121; - re.A[5 * 8 + 1] = 2.561846144803919; - re.A[5 * 8 + 2] = -11.627539656261098; - re.A[5 * 8 + 3] = -0.18268767659942256; - re.A[5 * 8 + 4] = 0.030198172008377946; - - re.A[6 * 8 + 0] = -7.502846399306121; - re.A[6 * 8 + 1] = 2.561846144803919; - re.A[6 * 8 + 2] = -11.627539656261098; - re.A[6 * 8 + 3] = -0.18268767659942256; - re.A[6 * 8 + 4] = 0.030198172008377946; - re.A[6 * 8 + 5] = 1.0; - - re.A[7 * 8 + 0] = -7.502846399306121; - re.A[7 * 8 + 1] = 2.561846144803919; - re.A[7 * 8 + 2] = -11.627539656261098; - re.A[7 * 8 + 3] = -0.18268767659942256; - re.A[7 * 8 + 4] = 0.030198172008377946; - re.A[7 * 8 + 5] = 1.0; - re.A[7 * 8 + 6] = 1.0; - - re.C[1 * 8 + 0] = -14.155112264123755; - - re.C[2 * 8 + 0] = -17.97296035885952; - re.C[2 * 8 + 1] = -2.859693295451294; - - re.C[3 * 8 + 0] = 147.12150275711716; - re.C[3 * 8 + 1] = -1.41221402718213; - re.C[3 * 8 + 2] = 71.68940251302358; - - re.C[4 * 8 + 0] = 165.43517024871676; - re.C[4 * 8 + 1] = -0.4592823456491126; - re.C[4 * 8 + 2] = 42.90938336958603; - re.C[4 * 8 + 3] = -5.961986721573306; - - re.C[5 * 8 + 0] = 24.854864614690072; - re.C[5 * 8 + 1] = -3.0009227002832186; - re.C[5 * 8 + 2] = 47.4931110020768; - re.C[5 * 8 + 3] = 5.5814197821558125; - re.C[5 * 8 + 4] = -0.6610691825249471; - - re.C[6 * 8 + 0] = 30.91273214028599; - re.C[6 * 8 + 1] = -3.1208243349937974; - re.C[6 * 8 + 2] = 77.79954646070892; - re.C[6 * 8 + 3] = 34.28646028294783; - re.C[6 * 8 + 4] = -19.097331116725623; - re.C[6 * 8 + 5] = -28.087943162872662; - - re.C[7 * 8 + 0] = 37.80277123390563; - re.C[7 * 8 + 1] = -3.2571969029072276; - re.C[7 * 8 + 2] = 112.26918849496327; - re.C[7 * 8 + 3] = 66.9347231244047; - re.C[7 * 8 + 4] = -40.06618937091002; - re.C[7 * 8 + 5] = -54.66780262877968; - re.C[7 * 8 + 6] = -9.48861652309627; - - re.H[0 * 8 + 0] = 25.948786856663858; - re.H[0 * 8 + 1] = -2.5579724845846235; - re.H[0 * 8 + 2] = 10.433815404888879; - re.H[0 * 8 + 3] = -2.3679251022685204; - re.H[0 * 8 + 4] = 0.524948541321073; - re.H[0 * 8 + 5] = 1.1241088310450404; - re.H[0 * 8 + 6] = 0.4272876194431874; - re.H[0 * 8 + 7] = -0.17202221070155493; - - re.H[1 * 8 + 0] = -9.91568850695171; - re.H[1 * 8 + 1] = -0.9689944594115154; - re.H[1 * 8 + 2] = 3.0438037242978453; - re.H[1 * 8 + 3] = -24.495224566215796; - re.H[1 * 8 + 4] = 20.176138334709044; - re.H[1 * 8 + 5] = 15.98066361424651; - re.H[1 * 8 + 6] = -6.789040303419874; - re.H[1 * 8 + 7] = -6.710236069923372; - - re.H[2 * 8 + 0] = 11.419903575922262; - re.H[2 * 8 + 1] = 2.8879645146136994; - re.H[2 * 8 + 2] = 72.92137995996029; - re.H[2 * 8 + 3] = 80.12511834622643; - re.H[2 * 8 + 4] = -52.072871366152654; - re.H[2 * 8 + 5] = -59.78993625266729; - re.H[2 * 8 + 6] = -0.15582684282751913; - re.H[2 * 8 + 7] = 4.883087185713722; + re.alpha_sum_[0] = 0.0; + re.alpha_sum_[1] = 0.6358126895828704; + re.alpha_sum_[2] = 0.4095798393397535; + re.alpha_sum_[3] = 0.9769306725060716; + re.alpha_sum_[4] = 0.4288403609558664; + re.alpha_sum_[5] = 1.0; + re.alpha_sum_[6] = 1.0; + re.alpha_sum_[7] = 1.0; + + re.gamma_sum_[0] = 0.21193756319429014; + re.gamma_sum_[1] = -0.42387512638858027; + re.gamma_sum_[2] = -0.3384627126235924; + re.gamma_sum_[3] = 1.8046452872882734; + re.gamma_sum_[4] = 2.325825639765069; + re.gamma_sum_[5] = 0.0; + re.gamma_sum_[6] = 0.0; + re.gamma_sum_[7] = 0.0; + + re.m_[0] = -7.502846399306121; + re.m_[1] = 2.561846144803919; + re.m_[2] = -11.627539656261098; + re.m_[3] = -0.18268767659942256; + re.m_[4] = 0.030198172008377946; + re.m_[5] = 1.0; + re.m_[6] = 1.0; + re.m_[7] = 1.0; + + re.e_[0] = 0.0; + re.e_[1] = 0.0; + re.e_[2] = 0.0; + re.e_[3] = 0.0; + re.e_[4] = 0.0; + re.e_[5] = 0.0; + re.e_[6] = 0.0; + re.e_[7] = 1.0; + + re.A_[1 * 8 + 0] = 3.0; + + re.A_[2 * 8 + 0] = 2.849394379747939; + re.A_[2 * 8 + 1] = 0.45842242204463923; + + re.A_[3 * 8 + 0] = -6.954028509809101; + re.A_[3 * 8 + 1] = 2.489845061869568; + re.A_[3 * 8 + 2] = -10.358996098473584; + + re.A_[4 * 8 + 0] = 2.8029986275628964; + re.A_[4 * 8 + 1] = 0.5072464736228206; + re.A_[4 * 8 + 2] = -0.3988312541770524; + re.A_[4 * 8 + 3] = -0.04721187230404641; + + re.A_[5 * 8 + 0] = -7.502846399306121; + re.A_[5 * 8 + 1] = 2.561846144803919; + re.A_[5 * 8 + 2] = -11.627539656261098; + re.A_[5 * 8 + 3] = -0.18268767659942256; + re.A_[5 * 8 + 4] = 0.030198172008377946; + + re.A_[6 * 8 + 0] = -7.502846399306121; + re.A_[6 * 8 + 1] = 2.561846144803919; + re.A_[6 * 8 + 2] = -11.627539656261098; + re.A_[6 * 8 + 3] = -0.18268767659942256; + re.A_[6 * 8 + 4] = 0.030198172008377946; + re.A_[6 * 8 + 5] = 1.0; + + re.A_[7 * 8 + 0] = -7.502846399306121; + re.A_[7 * 8 + 1] = 2.561846144803919; + re.A_[7 * 8 + 2] = -11.627539656261098; + re.A_[7 * 8 + 3] = -0.18268767659942256; + re.A_[7 * 8 + 4] = 0.030198172008377946; + re.A_[7 * 8 + 5] = 1.0; + re.A_[7 * 8 + 6] = 1.0; + + re.C_[1 * 8 + 0] = -14.155112264123755; + + re.C_[2 * 8 + 0] = -17.97296035885952; + re.C_[2 * 8 + 1] = -2.859693295451294; + + re.C_[3 * 8 + 0] = 147.12150275711716; + re.C_[3 * 8 + 1] = -1.41221402718213; + re.C_[3 * 8 + 2] = 71.68940251302358; + + re.C_[4 * 8 + 0] = 165.43517024871676; + re.C_[4 * 8 + 1] = -0.4592823456491126; + re.C_[4 * 8 + 2] = 42.90938336958603; + re.C_[4 * 8 + 3] = -5.961986721573306; + + re.C_[5 * 8 + 0] = 24.854864614690072; + re.C_[5 * 8 + 1] = -3.0009227002832186; + re.C_[5 * 8 + 2] = 47.4931110020768; + re.C_[5 * 8 + 3] = 5.5814197821558125; + re.C_[5 * 8 + 4] = -0.6610691825249471; + + re.C_[6 * 8 + 0] = 30.91273214028599; + re.C_[6 * 8 + 1] = -3.1208243349937974; + re.C_[6 * 8 + 2] = 77.79954646070892; + re.C_[6 * 8 + 3] = 34.28646028294783; + re.C_[6 * 8 + 4] = -19.097331116725623; + re.C_[6 * 8 + 5] = -28.087943162872662; + + re.C_[7 * 8 + 0] = 37.80277123390563; + re.C_[7 * 8 + 1] = -3.2571969029072276; + re.C_[7 * 8 + 2] = 112.26918849496327; + re.C_[7 * 8 + 3] = 66.9347231244047; + re.C_[7 * 8 + 4] = -40.06618937091002; + re.C_[7 * 8 + 5] = -54.66780262877968; + re.C_[7 * 8 + 6] = -9.48861652309627; + + re.H_[0 * 8 + 0] = 25.948786856663858; + re.H_[0 * 8 + 1] = -2.5579724845846235; + re.H_[0 * 8 + 2] = 10.433815404888879; + re.H_[0 * 8 + 3] = -2.3679251022685204; + re.H_[0 * 8 + 4] = 0.524948541321073; + re.H_[0 * 8 + 5] = 1.1241088310450404; + re.H_[0 * 8 + 6] = 0.4272876194431874; + re.H_[0 * 8 + 7] = -0.17202221070155493; + + re.H_[1 * 8 + 0] = -9.91568850695171; + re.H_[1 * 8 + 1] = -0.9689944594115154; + re.H_[1 * 8 + 2] = 3.0438037242978453; + re.H_[1 * 8 + 3] = -24.495224566215796; + re.H_[1 * 8 + 4] = 20.176138334709044; + re.H_[1 * 8 + 5] = 15.98066361424651; + re.H_[1 * 8 + 6] = -6.789040303419874; + re.H_[1 * 8 + 7] = -6.710236069923372; + + re.H_[2 * 8 + 0] = 11.419903575922262; + re.H_[2 * 8 + 1] = 2.8879645146136994; + re.H_[2 * 8 + 2] = 72.92137995996029; + re.H_[2 * 8 + 3] = 80.12511834622643; + re.H_[2 * 8 + 4] = -52.072871366152654; + re.H_[2 * 8 + 5] = -59.78993625266729; + re.H_[2 * 8 + 6] = -0.15582684282751913; + re.H_[2 * 8 + 7] = 4.883087185713722; return re; } diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index c1140c701..23d5f55b2 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -339,7 +339,7 @@ namespace GridKit { TestStatus success = true; - uint8_t expected_order = tab.order; + uint8_t expected_order = tab.order_; Model::TrigonometricDaeEvaluator model; model.allocate(); @@ -399,8 +399,8 @@ namespace GridKit } typename Rosenbrock::Parameters params; - params.starting_step = step_size; - params.max_steps = static_cast(ceil((final_time - 0.5) / step_size)) + 10; + params.starting_step_ = step_size; + params.max_steps_ = static_cast(ceil((final_time - 0.5) / step_size)) + 10; if (integrator.integrate(out_times, step_controller, params, out_cb)) { success = false; diff --git a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp index 34bf66138..245d27ffa 100644 --- a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp +++ b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp @@ -9,7 +9,7 @@ int main() GridKit::Testing::TestingResults result; GridKit::Testing::RosenbrockTests test; - result += test.test_order(Integrator::Rosenbrock::Tableau::lin_implicit_euler(), -1.0, -5.0); + result += test.test_order(Integrator::Rosenbrock::Tableau::linImplicitEuler(), -1.0, -5.0); result += test.test_order(Integrator::Rosenbrock::Tableau::rodas5p(), -0.5, -2.5); return result.summary(); From 0188944e01327cb28425bc5d37f93a3c58502360 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Thu, 9 Jul 2026 14:04:50 -0400 Subject: [PATCH 33/57] [skip ci] Add comment explaining confusing interaction --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 4091953f3..b7db3ae6b 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -493,6 +493,8 @@ namespace Integrator stats_.min_step_ = std::min(stats_.min_step_, step_size_); stats_.max_step_ = std::max(stats_.max_step_, step_size_); + // Shift y_new_ -> y_cur_ -> y_prev_ + // Then y_new_ is free to be replaced (contains old y_prev_) std::swap(y_prev_, y_cur_); std::swap(y_cur_, y_new_); } From 48bf1590507c4c3217a57633a3f94f5b7ae56c8a Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Thu, 9 Jul 2026 14:17:28 -0400 Subject: [PATCH 34/57] [skip ci] Update Documentation --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 6 +++--- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index b7db3ae6b..7b644a3e0 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -100,7 +100,7 @@ namespace Integrator * * @param other The other statistics to add to this one. * - * @todo Right now, the step numbers for \ref rejections and \ref skip_lu_steps are impossible to tell apart from the different simulations. + * @todo Right now, the step numbers for \ref rejections_ and \ref skip_lu_steps_ are impossible to tell apart from the different simulations. */ template typename Rosenbrock::Stats& Rosenbrock::Stats::operator+=(const Stats& other) @@ -217,7 +217,7 @@ namespace Integrator * * @param tab The tableau to be used for this integrator. Since tableaus contain `std::unique_ptr`, it must be moved into the integrator. * @param model The model to be simulated. Despite taking a pointer, this must be a valid pointer to an `Evaluator`. - * Must have \ref Evaluator::tag() set, and must be in Hessenberg form (\f(F(\dot y, y) = \dot y - f(y)\f)). + * Must have \ref GridKit::Model::Evaluator::tag() set, and must be in Hessenberg form (\f(F(\dot y, y) = \dot y - f(y)\f)). * @param lin_solver The linear solver to be used when constructing stages during simulation. The reference must remain valid for as long * as the Rosenbrock integrator lives. * @param vector_handler The vector handler to be used when simulating. The reference must remain valid for as long as the Rosenbrock @@ -321,7 +321,7 @@ namespace Integrator * * - Sets the simulation time to `t0` and copies the initial condition from \ref model_. * - Analyzes \ref model_ Jacobian sparsity and runs the preconditioner - * - Generates the mass matrix from \ref Evaluator::tag(). If the tag is not properly set, then initialization will fail. + * - Generates the mass matrix from \ref GridKit::Model::Evaluator::tag(). If the tag is not properly set, then initialization will fail. * - Resets \ref stats_. * * @note This method can fail. diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp index a9235f14c..2e5f14fbf 100644 --- a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -39,7 +39,7 @@ namespace Integrator } /** - * @brief A 5th order Rosenbrock method suitable for solving parabolic PDEs. Created in \cite{shortauthor}. + * @brief A 5th order Rosenbrock method suitable for solving parabolic PDEs. Created in \cite steinebach2023construction. * * */ From 39d95cd605a2792e810dfff08762e175179d5c8a Mon Sep 17 00:00:00 2001 From: Shaked Regev <35384901+shakedregev@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:23:01 -0400 Subject: [PATCH 35/57] Made screaming snake case convention explicit (#484) --- CHANGELOG.md | 1 + CONTRIBUTING.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb9da61b..c5d73ba7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ - Added portable `Vector` class and policy-based memory utilities. - Add new `LinearSolver` interface for linear solvers. - Added new `Rosenbrock` integrator. +- Clarified naming conventions for macros. ## v0.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a446018e..00bece554 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -285,6 +285,10 @@ name. Use all caps (screaming snake case). constexpr double EXP = 2.7183 // Yes ``` +### Macros + +Macros should use all caps, same as constants. + ### Enums (enumerated types) Always define `enum`s inside `GridKit` namespace. The `enum` name should From 0ddcd67d63a1c149247a4f8834fb061d8d7b6101 Mon Sep 17 00:00:00 2001 From: Shaked Regev <35384901+shakedregev@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:29:19 -0400 Subject: [PATCH 36/57] Apply suggestion from @shakedregev --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 7b644a3e0..ef1879dfb 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -599,7 +599,7 @@ namespace Integrator * * Apply the Rosenbrock scheme using the stored tableau. Each stage \f(u_i\f) is calculated as * - * \f[\left(J - \frac{1}{h\gamma} M\right)u_i = -f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j,\f] + * \f(\left(J - \frac{1}{h\gamma} M\right)u_i = -f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j,\f] * * and the next state \f(y_1\f) is calculated as * From 4670635e199e972ee4865028f3b503cb7f1b51d6 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 10 Jul 2026 14:07:32 -0400 Subject: [PATCH 37/57] Revert documentation delimiter change --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index ef1879dfb..7b644a3e0 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -599,7 +599,7 @@ namespace Integrator * * Apply the Rosenbrock scheme using the stored tableau. Each stage \f(u_i\f) is calculated as * - * \f(\left(J - \frac{1}{h\gamma} M\right)u_i = -f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j,\f] + * \f[\left(J - \frac{1}{h\gamma} M\right)u_i = -f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j,\f] * * and the next state \f(y_1\f) is calculated as * From 7c1b1cb961facdc9ec9acd02875b686bfe4bbd16 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 10 Jul 2026 15:04:19 -0400 Subject: [PATCH 38/57] Update constants and templates --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 82 +++++++++++-------- GridKit/Solver/Dynamic/Rosenbrock.hpp | 72 ++++++++-------- .../Solver/Dynamic/RosenbrockTests.hpp | 3 +- 3 files changed, 89 insertions(+), 68 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 7b644a3e0..929b15717 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -6,6 +6,7 @@ #include +#include #include /** @@ -200,11 +201,11 @@ namespace Integrator std::optional re; for (size_t j = 0; j < num_stages_; j++) { - if (e_[j] == 1.0 && !re) + if (e_[j] == GridKit::ONE && !re) { re = j; } - else if (e_[j] != 0.0) + else if (e_[j] != GridKit::ZERO) { return {}; } @@ -359,7 +360,7 @@ namespace Integrator std::unique_ptr mass = std::make_unique(model_->tag().size()); for (size_t i = 0; i < static_cast(model_->size()); i++) { - mass[i] = model_->tag()[i] ? 1.0 : 0.0; + mass[i] = model_->tag()[i] ? GridKit::ONE : GridKit::ZERO; } BUBBLE_FAIL(workspace_.mass_->copyFromExternal(mass.get(), memspace_, memspace_)); @@ -412,12 +413,15 @@ namespace Integrator * @return An error code, with 0 as success. */ template - int Rosenbrock::integrate(const std::vector& out_times, - StepController& step_controller, + int Rosenbrock::integrate(const std::vector& out_times, + StepController& step_controller, Parameters params, - std::optional> out_cb, + std::optional> out_cb, std::optional> step_cb) { + constexpr RealT ONE = GridKit::ONE; + constexpr RealT ZERO = GridKit::ZERO; + skip_lu_ = false; skip_f_ = false; @@ -518,7 +522,7 @@ namespace Integrator // instead keep the step size the same and use time-delay Jacobian. // TODO: configure upper bound here double step_gain = next_step_size / step_size_; - if (params.skip_lu_ && step_gain >= 1 && step_gain <= 1.2) + if (params.skip_lu_ && step_gain >= ONE && step_gain <= 1.2) { skip_lu_ = true; } @@ -533,7 +537,7 @@ namespace Integrator if (step_cb) { BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(current_time_, 0.0); + model_->updateTime(current_time_, ZERO); (*step_cb)(StepInfo{ .sim_time_ = current_time_, @@ -554,7 +558,7 @@ namespace Integrator { // Theta = (t - t0) / h = (t - t1) / h + 1 // current_time_ is t1 here - double theta = (out_time - current_time_) / prev_step_size_ + 1; + RealT theta = (out_time - current_time_) / prev_step_size_ + ONE; // Generate output at the appropriate time. if (tab_.hasDenseOutput()) @@ -579,7 +583,7 @@ namespace Integrator if (out_cb) { BUBBLE_FAIL(y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(out_time, 0.0); + model_->updateTime(out_time, ZERO); (*out_cb)(out_time); } @@ -647,8 +651,11 @@ namespace Integrator * @return An error code, with 0 as success. */ template - int Rosenbrock::timeStep(double t0, double dt) + int Rosenbrock::timeStep(RealT t0, RealT dt) { + constexpr RealT ZERO = GridKit::ZERO; + constexpr RealT MINUS_ONE = GridKit::MINUS_ONE; + // A flag to keep track of if y0 (stored in y_cur_) has been copied in to the model already, to avoid double-copying // for evaluating the Jacobian and residual on stage 1 (both evaluated at y0). bool y0_copied = false; @@ -663,7 +670,7 @@ namespace Integrator // GridKit, like IDA, expects to evaluate the Jacobian J = df/dy + alpha * df/dy', // so we need a negative here since df/dy' = M. - model_->updateTime(t0, -1.0 / (dt * tab_.gamma_)); + model_->updateTime(t0, MINUS_ONE / (dt * tab_.gamma_)); BUBBLE_FAIL(model_->evaluateJacobian()); GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); @@ -703,7 +710,7 @@ namespace Integrator BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); y0_copied = true; } - model_->updateTime(t0, 0.0); + model_->updateTime(t0, ZERO); BUBBLE_FAIL(model_->evaluateResidual()); BUBBLE_FAIL(workspace_.RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); vector_handler_.scal(-1, workspace_.RHS_first_stage_.get(), memspace_); @@ -722,7 +729,7 @@ namespace Integrator // We can sometimes reuse asum from the previous stage if (i > 1 && tab_.canReuseAsum(i)) { - if (tab_.A_[tab_.num_stages_ * i + i - 1] != 0.0) + if (tab_.A_[tab_.num_stages_ * i + i - 1] != ZERO) vector_handler_.axpy(tab_.A_[tab_.num_stages_ * i + i - 1], workspace_.stages_[i - 1].get(), workspace_.asum_.get(), memspace_); } else @@ -731,7 +738,7 @@ namespace Integrator for (size_t j = 0; j < i; j++) { - if (tab_.A_[tab_.num_stages_ * i + j] != 0.0) + if (tab_.A_[tab_.num_stages_ * i + j] != ZERO) vector_handler_.axpy(tab_.A_[tab_.num_stages_ * i + j], workspace_.stages_[j].get(), workspace_.asum_.get(), memspace_); } } @@ -741,7 +748,7 @@ namespace Integrator BUBBLE_FAIL(workspace_.csum_->setToZero(memspace_)); for (size_t j = 0; j < i; j++) { - if (tab_.C_[i * tab_.num_stages_ + j] != 0.0) + if (tab_.C_[i * tab_.num_stages_ + j] != ZERO) { vector_handler_.axpy(tab_.C_[i * tab_.num_stages_ + j] / dt, workspace_.stages_[j].get(), workspace_.csum_.get(), memspace_); } @@ -749,14 +756,13 @@ namespace Integrator // TODO: non-autonomous model BUBBLE_FAIL(workspace_.asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(t0 + tab_.alpha_sum_[i] * dt, 0.0); + model_->updateTime(t0 + tab_.alpha_sum_[i] * dt, ZERO); BUBBLE_FAIL(model_->evaluateResidual()); workspace_.RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); - // TODO: examine if this -1 is correct - vector_handler_.scal(-1, workspace_.RHS_.get(), memspace_); + vector_handler_.scal(MINUS_ONE, workspace_.RHS_.get(), memspace_); vector_handler_.scal(workspace_.mass_.get(), workspace_.csum_.get(), memspace_); - vector_handler_.axpy(-1, workspace_.csum_.get(), workspace_.RHS_.get(), memspace_); + vector_handler_.axpy(MINUS_ONE, workspace_.csum_.get(), workspace_.RHS_.get(), memspace_); resolve_rhs_->setData(workspace_.RHS_->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); resolve_lhs_->setData(workspace_.stages_[i]->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); @@ -779,7 +785,7 @@ namespace Integrator for (size_t j = 0; j < tab_.num_stages_; j++) { - if (tab_.m_[j] != 0.0) + if (tab_.m_[j] != ZERO) { vector_handler_.axpy(tab_.m_[j], workspace_.stages_[j].get(), y_new_.get(), memspace_); } @@ -833,7 +839,7 @@ namespace Integrator vector_handler_.scal(tab_.e_[0], workspace_.err_est_.get(), memspace_); for (size_t j = 1; j < tab_.num_stages_; j++) { - if (tab_.e_[j] != 0.0) + if (tab_.e_[j] != GridKit::ZERO) { vector_handler_.axpy(tab_.e_[j], workspace_.stages_[j].get(), workspace_.err_est_.get(), memspace_); } @@ -906,8 +912,10 @@ namespace Integrator * @return An error code, with 0 as success. */ template - int Rosenbrock::interpDense(double theta) + int Rosenbrock::interpDense(RealT theta) { + constexpr RealT ONE = GridKit::ONE; + if (tab_.order_ > 2) { BUBBLE_FAIL(y_interp_->copyFromExternal(*dense_coeff_[tab_.order_ - 3], memspace_, memspace_)); @@ -915,21 +923,21 @@ namespace Integrator for (size_t i = 1; i < static_cast(tab_.order_ - 2); i++) { vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(1.0, dense_coeff_[tab_.order_ - 3 - i].get(), y_interp_.get(), memspace_); + vector_handler_.axpy(ONE, dense_coeff_[tab_.order_ - 3 - i].get(), y_interp_.get(), memspace_); } // TODO: This scal can be removed and absorbed into the next axpy, except that it currently isn't possible to put a scalar // multiple on the y term. - vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); + vector_handler_.scal(ONE - theta, y_interp_.get(), memspace_); } else { BUBBLE_FAIL(y_interp_->setToZero(memspace_)); } - vector_handler_.axpy(1.0, y_cur_.get(), y_interp_.get(), memspace_); + vector_handler_.axpy(ONE, y_cur_.get(), y_interp_.get(), memspace_); vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(1 - theta, y_prev_.get(), y_interp_.get(), memspace_); + vector_handler_.axpy(ONE - theta, y_prev_.get(), y_interp_.get(), memspace_); return 0; } @@ -940,11 +948,12 @@ namespace Integrator * \f[h_{new} = h * \min \left\{fac_{max}, \max\left\{fac_{min}, fac_{scale} \cdot e ^{-1/p}\right\}\right\}.\f] * */ - StepControl AdaptiveStep::nextStep(double err, StepControl prev_step, uint8_t method_order) + template + StepControl AdaptiveStep::nextStep(RealT err, StepControl prev_step, uint8_t method_order) { - StepControl next_step = prev_step; + StepControl next_step = prev_step; - double h_mult = std::min(params_.fac_max_, std::max(params_.fac_scale_ * std::pow(err, -1.0 / method_order), params_.fac_min_)); + double h_mult = std::min(params_.fac_max_, std::max(params_.fac_scale_ * std::pow(err, GridKit::MINUS_ONE / method_order), params_.fac_min_)); next_step.accept_ = err <= 1; next_step.step_size_ *= h_mult; @@ -956,9 +965,10 @@ namespace Integrator * @brief Fixed step - accept every step, no matter the error, and keep the step size the same. * */ - StepControl FixedStep::nextStep([[maybe_unused]] double err, StepControl prev_step, [[maybe_unused]] uint8_t method_order) + template + StepControl FixedStep::nextStep([[maybe_unused]] RealT err, StepControl prev_step, [[maybe_unused]] uint8_t method_order) { - return StepControl{ + return StepControl{ .accept_ = true, .step_size_ = prev_step.step_size_, }; @@ -980,7 +990,7 @@ namespace Integrator * @see `Rosenbrock::errorEstimate()` */ template - double InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const + InfNorm::RealT InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const { if (int err_code = workspace_.out_->copyFromExternal(&err, memspace, memspace)) { @@ -1002,11 +1012,15 @@ namespace Integrator // TODO: This scal shouldn't be necessary, but axpy doesn't support scaling the y parameter. In the future, // the scaling should be able to be put on the next axpy. handler.scal(params_.rel_tol_, workspace_.scale_.get(), memspace); - handler.axpy(1.0, params_.abs_tol_.get(), workspace_.scale_.get(), memspace); + handler.axpy(GridKit::ONE, params_.abs_tol_.get(), workspace_.scale_.get(), memspace); handler.diagSolve(workspace_.scale_.get(), workspace_.out_.get(), memspace); return handler.amax(workspace_.out_.get(), memspace); } template class Rosenbrock; + + template class FixedStep; + + template class AdaptiveStep; } // namespace Integrator diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 9daa86d35..2c6d6649e 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -25,6 +25,7 @@ namespace Integrator * @brief Define control flow for `StepController`s to be able to control the step size of a `Rosenbrock` integrator. * */ + template struct StepControl { /** @@ -32,12 +33,12 @@ namespace Integrator * the next state and re-step with the new `step_size`. * */ - bool accept_; + bool accept_; /** * @brief The step size the next step should take. * */ - double step_size_; + RealT step_size_; }; /** @@ -46,6 +47,7 @@ namespace Integrator * * @todo It may be best to have \ref usesError() return a reference to the \ref ErrorNorm that should be used. */ + template class StepController { public: @@ -58,12 +60,12 @@ namespace Integrator * @param method_order The order of the method being used. * @return StepControl */ - virtual StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) = 0; + virtual StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) = 0; /** * @brief Return whether or not the `nextStep` method implementation uses the `err` parameter. If `false`, this parameter is not calculated. * */ - virtual bool usesError() const = 0; + virtual bool usesError() const = 0; }; /** @@ -74,6 +76,7 @@ namespace Integrator class ErrorNorm { using State = GridKit::LinearAlgebra::Vector; + using RealT = typename GridKit::ScalarTraits::RealT; public: /** @@ -85,11 +88,11 @@ namespace Integrator * @param yprev The state from the previous step. Can be used for proper relative error normalization. * @param handler A vector handler which can be used to facilitate vector operations. * @param memspace The memory space which vector operations should be performed in/. - * @return double The error. + * @return The error. * * @todo Allow this method to fail, since it will likely involve linear algebra calls. */ - virtual double errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const = 0; + virtual RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const = 0; }; /** @@ -116,22 +119,22 @@ namespace Integrator * @brief The simulation time at the beginning of the step * */ - double sim_time_; + RealT sim_time_; /** * @brief The size of the step. * */ - double step_size_; + RealT step_size_; /** * @brief The size of the next step, as governed by the current `StepController` in use. * */ - double next_step_size_; + RealT next_step_size_; /** * @brief The estimated error made by the step, as calculated by the current `ErrorNorm` in use. * */ - double err_est_; + RealT err_est_; /** * @brief The step number, starting at 1. * @@ -202,12 +205,12 @@ namespace Integrator * @brief Minimum step size. * */ - double min_step_ = INFINITY; + RealT min_step_ = INFINITY; /** * @brief Maximum step size. * */ - double max_step_ = 0; + RealT max_step_ = 0; std::string report() const; Stats& operator+=(const Stats& other); @@ -224,7 +227,7 @@ namespace Integrator * * @todo Consider adding a starting step size selector to select this automatically. */ - double starting_step_ = 1e-5; + RealT starting_step_ = 1e-5; /** * @brief The maximum number of steps the integrator should take. If the integrator has not reached the final time before * taking this many steps, then integration is stopped. For more details, see `integrate()`. @@ -372,13 +375,13 @@ namespace Integrator * the initial step after resuming. * */ - double step_size_ = 0; + RealT step_size_ = 0; /** * @brief The step size of the previous step. Used for operations which need to be done on the current step, but step size * control for the next step has already been performed. * */ - double prev_step_size_ = 0; + RealT prev_step_size_ = 0; /** * @brief Whether or not the integrator should attempt to skip Jacobian decomposition on the next step. Controlled by the * time stepping algorithm in \link integrate() \endlink . Generally, this should only be set if we suspect the Jacobian for the @@ -387,21 +390,21 @@ namespace Integrator * as the previous step. * */ - bool skip_lu_ = false; + bool skip_lu_ = false; /** * @brief Whether or not the integrator should attempt to skip the residual function evaluation of the first stage on the * next step. This should only be used when a step is rejected and the residual function is evaluated at the exact * same arguments as the previous step. Then \ref RHS_first_stage_ can be re-used rather than re-calculated. * */ - bool skip_f_ = false; + bool skip_f_ = false; /** * @brief Keeps track of whether or not the integrator currently has valid dense coefficients. * i.e. they have been computed and haven't been invalidated by taking another step. This can be used to avoid * re-computing dense coefficients when interpolating states multiple times in one step. * */ - bool dense_coefficients_valid_ = false; + bool dense_coefficients_valid_ = false; /** * @brief The tableau of Rosenbrock coefficients currently being used by the integrator. @@ -441,7 +444,7 @@ namespace Integrator * @brief The current simulation time. * */ - double current_time_; + RealT current_time_; /** * @brief The state from last step. @@ -504,10 +507,10 @@ namespace Integrator int initializeSimulation(RealT t0); [[nodiscard("May fail. Check error code.")]] - int integrate(const std::vector& out_times, - StepController& step_controller, + int integrate(const std::vector& out_times, + StepController& step_controller, Parameters params = {}, - std::optional> out_cb = {}, + std::optional> out_cb = {}, std::optional> step_cb = {}); private: @@ -593,7 +596,7 @@ namespace Integrator public: [[nodiscard("May fail. Check error code.")]] - int timeStep(double t0, double dt); + int timeStep(RealT t0, RealT dt); State& errorEstimate() const; @@ -601,7 +604,7 @@ namespace Integrator int calcDenseCoeff(); [[nodiscard("May fail. Check error code.")]] - int interpDense(double theta); + int interpDense(RealT theta); }; /** @@ -609,7 +612,8 @@ namespace Integrator * based on an error estimate. * */ - class AdaptiveStep : public StepController + template + class AdaptiveStep : public StepController { /** * @brief Parameters for the step controller. @@ -625,7 +629,7 @@ namespace Integrator * @note Should be between 0 and 1. * */ - double fac_min_ = 0.2; + RealT fac_min_ = 0.2; /** * @brief The maximum multiple by which the step size can be multiplied to obtain the new step size. * Decreasing this will make the integrator more conservative in selecting the step size - @@ -634,7 +638,7 @@ namespace Integrator * @note Should be greater than 1. * */ - double fac_max_ = 5.0; + RealT fac_max_ = 5.0; /** * @brief A "fudge factor" introduced to decrease risk of failing a step. The larger the fudge factor, * the more likely steps will fail, but fewer steps will be taken. @@ -642,7 +646,7 @@ namespace Integrator * @note Should be between 0 and 1. * */ - double fac_scale_ = 0.9; + RealT fac_scale_ = 0.9; } params_; public: @@ -651,7 +655,7 @@ namespace Integrator { } - StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) final; + StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; /** * @brief This controller uses error estimates. @@ -673,9 +677,10 @@ namespace Integrator * To set the fixed size, set the `Rosenbrock::Parameters::starting_step` parameter. * */ - class FixedStep : public StepController + template + class FixedStep : public StepController { - StepControl nextStep(double err, StepControl prev_step, uint8_t method_order) final; + StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; /** * @brief This controller does not use error estimates. @@ -698,6 +703,7 @@ namespace Integrator class InfNorm : public ErrorNorm { using State = GridKit::LinearAlgebra::Vector; + using RealT = ErrorNorm::RealT; /** * @brief A workspace for the linear algebra operations required to calculate the norm. @@ -741,7 +747,7 @@ namespace Integrator * the solution's maximum element than this. * */ - double rel_tol_; + RealT rel_tol_; } params_; InfNorm(Parameters&& params) @@ -749,6 +755,6 @@ namespace Integrator { } - double errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const final; + RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const final; }; } // namespace Integrator diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index 23d5f55b2..8a241314d 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -333,6 +333,7 @@ namespace GridKit class RosenbrockTests { using Rosenbrock = Integrator::Rosenbrock; + using RealT = typename GridKit::ScalarTraits::RealT; public: TestOutcome test_order(Rosenbrock::Tableau&& tab, double step_exponent_lower, double step_exponent_upper) @@ -358,7 +359,7 @@ namespace GridKit return success.report(__func__); } - Integrator::FixedStep step_controller; + Integrator::FixedStep step_controller; double final_time = 2.0; std::vector out_times = {final_time}; From c5e345b42e479b2e7267f8dd4bab15cbf3299d04 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 10 Jul 2026 15:06:34 -0400 Subject: [PATCH 39/57] Remove SUNDIALS type --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 929b15717..2621d5cde 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -4,8 +4,6 @@ #include #include -#include - #include #include @@ -1018,9 +1016,9 @@ namespace Integrator return handler.amax(workspace_.out_.get(), memspace); } - template class Rosenbrock; + template class Rosenbrock; - template class FixedStep; + template class FixedStep; - template class AdaptiveStep; + template class AdaptiveStep; } // namespace Integrator From 4086a9fc1950b3e1b91e1b167f556f2297473e6a Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 10 Jul 2026 15:07:04 -0400 Subject: [PATCH 40/57] Fix header include templating --- tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp index 245d27ffa..93debd641 100644 --- a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp +++ b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp @@ -1,4 +1,5 @@ -#include "GridKit/Solver/Dynamic/Rosenbrock.hpp" +#include + #include "RosenbrockTests.hpp" int main() From fc3f548a4c7dba2493fd82709718f40738f76fb7 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 10 Jul 2026 15:07:57 -0400 Subject: [PATCH 41/57] Update rosenbrock test cmake target name --- tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index ca1dc1a35..ff1d6c64d 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -10,9 +10,9 @@ if(TARGET SUNDIALS::idas) endif() if(GRIDKIT_ENABLE_RESOLVE) - add_executable(test_ros runRosenbrockTests.cpp) - target_link_libraries(test_ros GridKit::rosenbrock GridKit::testing) - add_test(NAME ROSTest COMMAND $) + add_executable(test_rosenbrock runRosenbrockTests.cpp) + target_link_libraries(test_rosenbrock GridKit::rosenbrock GridKit::testing) + add_test(NAME ROSTest COMMAND $) - install(TARGETS test_ros RUNTIME DESTINATION bin) + install(TARGETS test_rosenbrock RUNTIME DESTINATION bin) endif() From 8546dda8481132dc133f36d04e916fa285b4e94a Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 10 Jul 2026 16:16:46 -0400 Subject: [PATCH 42/57] Update ReSolve dependency --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index 2621d5cde..b08486ccf 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -347,7 +347,11 @@ namespace Integrator GridKit::memory::memorySpaceAsResolve(memspace_))); BUBBLE_FAIL(lin_solver_.setMatrix(workspace_.jacobian_.get())); BUBBLE_FAIL(lin_solver_.analyze()); - BUBBLE_FAIL(lin_solver_.preconditionerSetup()); + + // TODO: This function needs to be called to properly use a preconditioner in ReSolve (if there is any), but currently will error + // if there is no preconditioner configured. Once we can detect if a preconditioner is configured, we can restore this functionality. + // Also, we will always want to use *right* preconditioning. + // BUBBLE_FAIL(lin_solver_.preconditionerSetup("right")); if (model_->tag().size() != static_cast(model_->size())) { @@ -454,14 +458,14 @@ namespace Integrator err = err_norm_->errorNorm(err_vec, *y_new_, *y_cur_, vector_handler_, memspace_); } - StepControl next_step = step_controller.nextStep(err, - StepControl{ - .accept_ = prev_accept, - .step_size_ = step_size_, - }, - tab_.order_); - prev_accept = next_step.accept_; - next_step_size = next_step.step_size_; + StepControl next_step = step_controller.nextStep(err, + StepControl{ + .accept_ = prev_accept, + .step_size_ = step_size_, + }, + tab_.order_); + prev_accept = next_step.accept_; + next_step_size = next_step.step_size_; if (prev_accept) { From 21affa72365cb53650356be369513e853d5113ad Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 10 Jul 2026 17:52:20 -0400 Subject: [PATCH 43/57] Remove extra sundials include --- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp index 2e5f14fbf..75d7be825 100644 --- a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -1,5 +1,3 @@ -#include - #include "Rosenbrock.hpp" namespace Integrator @@ -200,5 +198,5 @@ namespace Integrator return re; } - template class Rosenbrock; + template class Rosenbrock; } // namespace Integrator From bfbd34ab5bf9cdf68d2f44f4365ac329f4a82cac Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 10 Jul 2026 17:53:14 -0400 Subject: [PATCH 44/57] Update test name --- tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index ff1d6c64d..625736c49 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -12,7 +12,7 @@ endif() if(GRIDKIT_ENABLE_RESOLVE) add_executable(test_rosenbrock runRosenbrockTests.cpp) target_link_libraries(test_rosenbrock GridKit::rosenbrock GridKit::testing) - add_test(NAME ROSTest COMMAND $) + add_test(NAME RosenbrockTest COMMAND $) install(TARGETS test_rosenbrock RUNTIME DESTINATION bin) endif() From 2be100ab0b02cf98835ffeec32a22835091dd7a5 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Sun, 12 Jul 2026 09:38:56 -0400 Subject: [PATCH 45/57] remove extra ReSolve memory space --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 8 ++++---- GridKit/Solver/Dynamic/Rosenbrock.hpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index b08486ccf..ad3b38722 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -992,19 +992,19 @@ namespace Integrator * @see `Rosenbrock::errorEstimate()` */ template - InfNorm::RealT InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const + InfNorm::RealT InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const { if (int err_code = workspace_.out_->copyFromExternal(&err, memspace, memspace)) { - throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); } if (int err_code = workspace_.scale_->copyFromExternal(&y, memspace, memspace)) { - throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); } if (int err_code = workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace)) { - throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); } handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 2c6d6649e..3f36d6af0 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -755,6 +755,6 @@ namespace Integrator { } - RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, ReSolve::memory::MemorySpace memspace) const final; + RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const final; }; } // namespace Integrator From 58e9137299027b505f850f1ae706bc033b5cc0b6 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 13 Jul 2026 11:55:54 -0400 Subject: [PATCH 46/57] Break Rosenbrock helper classes into own files --- GridKit/Solver/Dynamic/AdaptiveStep.cpp | 29 +++ GridKit/Solver/Dynamic/AdaptiveStep.hpp | 69 ++++++ GridKit/Solver/Dynamic/CMakeLists.txt | 33 ++- GridKit/Solver/Dynamic/ErrorNorm.hpp | 35 +++ GridKit/Solver/Dynamic/FixedStep.cpp | 19 ++ GridKit/Solver/Dynamic/FixedStep.hpp | 31 +++ GridKit/Solver/Dynamic/InfNorm.cpp | 52 ++++ GridKit/Solver/Dynamic/InfNorm.hpp | 75 ++++++ GridKit/Solver/Dynamic/Rosenbrock.cpp | 80 ------ GridKit/Solver/Dynamic/Rosenbrock.hpp | 229 +----------------- GridKit/Solver/Dynamic/StepControl.hpp | 24 ++ GridKit/Solver/Dynamic/StepController.hpp | 35 +++ tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 2 +- .../Solver/Dynamic/RosenbrockTests.hpp | 3 +- 14 files changed, 402 insertions(+), 314 deletions(-) create mode 100644 GridKit/Solver/Dynamic/AdaptiveStep.cpp create mode 100644 GridKit/Solver/Dynamic/AdaptiveStep.hpp create mode 100644 GridKit/Solver/Dynamic/ErrorNorm.hpp create mode 100644 GridKit/Solver/Dynamic/FixedStep.cpp create mode 100644 GridKit/Solver/Dynamic/FixedStep.hpp create mode 100644 GridKit/Solver/Dynamic/InfNorm.cpp create mode 100644 GridKit/Solver/Dynamic/InfNorm.hpp create mode 100644 GridKit/Solver/Dynamic/StepControl.hpp create mode 100644 GridKit/Solver/Dynamic/StepController.hpp diff --git a/GridKit/Solver/Dynamic/AdaptiveStep.cpp b/GridKit/Solver/Dynamic/AdaptiveStep.cpp new file mode 100644 index 000000000..0e1db9ba0 --- /dev/null +++ b/GridKit/Solver/Dynamic/AdaptiveStep.cpp @@ -0,0 +1,29 @@ +#include "AdaptiveStep.hpp" + +#include + +#include + +namespace Integrator +{ + /** + * @brief Standard textbook adaptive controller. Accept if `err <= 1` and use + * + * \f[h_{new} = h * \min \left\{fac_{max}, \max\left\{fac_{min}, fac_{scale} \cdot e ^{-1/p}\right\}\right\}.\f] + * + */ + template + StepControl AdaptiveStep::nextStep(RealT err, StepControl prev_step, uint8_t method_order) + { + StepControl next_step = prev_step; + + double h_mult = std::min(params_.fac_max_, std::max(params_.fac_scale_ * std::pow(err, GridKit::MINUS_ONE / method_order), params_.fac_min_)); + + next_step.accept_ = err <= 1; + next_step.step_size_ *= h_mult; + + return next_step; + } + + template class AdaptiveStep; +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/AdaptiveStep.hpp b/GridKit/Solver/Dynamic/AdaptiveStep.hpp new file mode 100644 index 000000000..246ab554e --- /dev/null +++ b/GridKit/Solver/Dynamic/AdaptiveStep.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include + +namespace Integrator +{ + + /** + * @brief A simple textbook adaptive `StepController` which seeks to meet a relative and absolute tolerance + * based on an error estimate. + * + */ + template + class AdaptiveStep : public StepController + { + /** + * @brief Parameters for the step controller. + * + */ + struct Parameters + { + /** + * @brief The minimum multiple by which the step size can be multiplied to obtain the new step size. + * Increasing this can allow the integrator to be slightly more conservative in selecting the step size + * - decreasing the number of steps taken but increasing the risk of failing the next step. + * + * @note Should be between 0 and 1. + * + */ + RealT fac_min_ = 0.2; + /** + * @brief The maximum multiple by which the step size can be multiplied to obtain the new step size. + * Decreasing this will make the integrator more conservative in selecting the step size - + * increasing the number of steps taken but decreasing the risk of failing the next step. + * + * @note Should be greater than 1. + * + */ + RealT fac_max_ = 5.0; + /** + * @brief A "fudge factor" introduced to decrease risk of failing a step. The larger the fudge factor, + * the more likely steps will fail, but fewer steps will be taken. + * + * @note Should be between 0 and 1. + * + */ + RealT fac_scale_ = 0.9; + } params_; + + public: + AdaptiveStep(const Parameters& params) + : params_(params) + { + } + + StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; + + /** + * @brief This controller uses error estimates. + * + * @see `nextStep()` + * + */ + constexpr bool usesError() const final + { + return true; + } + }; +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index 88e3e5e6c..c598fddff 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -5,13 +5,11 @@ # - Cameron Rutherford #]] -set(_install_headers DynamicSolver.hpp Ida.hpp Rosenbrock.hpp) - if(GRIDKIT_ENABLE_SUNDIALS_SPARSE) gridkit_add_library( solvers_dyn SOURCES Ida.cpp - HEADERS ${_install_headers} + HEADERS Ida.hpp LINK_LIBRARIES PUBLIC SUNDIALS::nvecserial @@ -29,7 +27,7 @@ else() gridkit_add_library( solvers_dyn SOURCES Ida.cpp - HEADERS ${_install_headers} + HEADERS Ida.hpp LINK_LIBRARIES PUBLIC SUNDIALS::nvecserial @@ -47,9 +45,9 @@ if(GRIDKIT_ENABLE_RESOLVE) gridkit_add_library( rosenbrock SOURCES Rosenbrock.cpp RosenbrockTableaus.cpp - HEADERS ${_install_headers} + HEADERS Rosenbrock.hpp LINK_LIBRARIES - PUBLIC + PRIVATE ReSolve::ReSolve PUBLIC GridKit::dense_vector @@ -59,4 +57,27 @@ if(GRIDKIT_ENABLE_RESOLVE) GridKit::utilities_logger PUBLIC GridKit::sparse_matrix) + + gridkit_add_library( + inf_norm + SOURCES InfNorm.cpp + HEADERS InfNorm.hpp + LINK_LIBRARIES + PRIVATE + ReSolve::ReSolve + PUBLIC + GridKit::dense_vector + ) endif() + +gridkit_add_library( + adaptive_step + SOURCES AdaptiveStep.cpp + HEADERS AdaptiveStep.hpp +) + +gridkit_add_library( + fixed_step + SOURCES FixedStep.cpp + HEADERS FixedStep.hpp +) \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/ErrorNorm.hpp b/GridKit/Solver/Dynamic/ErrorNorm.hpp new file mode 100644 index 000000000..3f5f88d8a --- /dev/null +++ b/GridKit/Solver/Dynamic/ErrorNorm.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace Integrator +{ + /** + * @brief Interface for error norms. Used to calculate the `err` parameter in `StepController::nextStep` based on a residual state error vector. + * + */ + template + class ErrorNorm + { + using State = GridKit::LinearAlgebra::Vector; + using RealT = typename GridKit::ScalarTraits::RealT; + + public: + /** + * @brief Calculate an error to be used by a step controller. Typically, an error > 1 indicates an error which does not meet tolerances, while + * an error < 1 indicates an error which meets tolerances. For that reason, tolerances should be included in the calculation of the error. + * + * @param err The state error residual being measured. + * @param y The state that the error was calculated from. Can be used for proper relative error normalization. + * @param yprev The state from the previous step. Can be used for proper relative error normalization. + * @param handler A vector handler which can be used to facilitate vector operations. + * @param memspace The memory space which vector operations should be performed in/. + * @return The error. + * + * @todo Allow this method to fail, since it will likely involve linear algebra calls. + */ + virtual RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const = 0; + }; +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/FixedStep.cpp b/GridKit/Solver/Dynamic/FixedStep.cpp new file mode 100644 index 000000000..68bbdc94c --- /dev/null +++ b/GridKit/Solver/Dynamic/FixedStep.cpp @@ -0,0 +1,19 @@ +#include "FixedStep.hpp" + +namespace Integrator +{ + /** + * @brief Fixed step - accept every step, no matter the error, and keep the step size the same. + * + */ + template + StepControl FixedStep::nextStep([[maybe_unused]] RealT err, StepControl prev_step, [[maybe_unused]] uint8_t method_order) + { + return StepControl{ + .accept_ = true, + .step_size_ = prev_step.step_size_, + }; + } + + template class FixedStep; +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/FixedStep.hpp b/GridKit/Solver/Dynamic/FixedStep.hpp new file mode 100644 index 000000000..bf02747c2 --- /dev/null +++ b/GridKit/Solver/Dynamic/FixedStep.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace Integrator +{ + /** + * @brief A fixed step controller which doesn't change the step size and accepts every step. + * Useful if you know what time scale your simulation operates on apriori and you're + * using a method without an embedded error controller. + * + * To set the fixed size, set the `Rosenbrock::Parameters::starting_step` parameter. + * + */ + template + class FixedStep : public StepController + { + StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; + + /** + * @brief This controller does not use error estimates. + * + * @see `nextStep()` + * + */ + constexpr bool usesError() const final + { + return false; + } + }; +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/InfNorm.cpp b/GridKit/Solver/Dynamic/InfNorm.cpp new file mode 100644 index 000000000..069b18149 --- /dev/null +++ b/GridKit/Solver/Dynamic/InfNorm.cpp @@ -0,0 +1,52 @@ +#include "InfNorm.hpp" + +#include + +#include + +namespace Integrator +{ + /** + * @brief Calculate the infinity error norm as + * + * \f[e = \max_i\frac{|\hat{e}_i|}{Atol_i + Rtol \cdot \max\{|y_{0i}|, |y_{1i}|\}},\f] + * + * where \f(y_0\f) is the initial state, \f(y_1\f) is the next state, and \f(\hat{e}\f) is the estimated error made in calculating + * the next state (typically \f(\hat{e} = y_1 - \hat{y}_1\f) for some different-order approximation \f(\hat{y}_1\f)). + * + * @param err \f(\hat{e}\f) in the above formula. + * @param y \f(y_1\f) in the above formula. + * @param yprev \f(y_0\f) in the above formula. + * @param handler The handler to be used for performing linear algebra operations. + * @param memspace The memory space to be used for performing linear lagebra operations. + * @see `Rosenbrock::errorEstimate()` + */ + template + InfNorm::RealT InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const + { + if (int err_code = workspace_.out_->copyFromExternal(&err, memspace, memspace)) + { + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); + } + if (int err_code = workspace_.scale_->copyFromExternal(&y, memspace, memspace)) + { + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); + } + if (int err_code = workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace)) + { + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); + } + + handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); + handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); + handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.scale_.get(), memspace); + + // TODO: This scal shouldn't be necessary, but axpy doesn't support scaling the y parameter. In the future, + // the scaling should be able to be put on the next axpy. + handler.scal(params_.rel_tol_, workspace_.scale_.get(), memspace); + handler.axpy(GridKit::ONE, params_.abs_tol_.get(), workspace_.scale_.get(), memspace); + handler.diagSolve(workspace_.scale_.get(), workspace_.out_.get(), memspace); + + return handler.amax(workspace_.out_.get(), memspace); + } +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/InfNorm.hpp b/GridKit/Solver/Dynamic/InfNorm.hpp new file mode 100644 index 000000000..2c4053a3a --- /dev/null +++ b/GridKit/Solver/Dynamic/InfNorm.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include + +#include +#include + +namespace Integrator +{ + + /** + * @brief The infinity error norm, which requires the error in every component in the system + * to meet tolerance. + * + */ + template + class InfNorm : public ErrorNorm + { + using State = GridKit::LinearAlgebra::Vector; + using RealT = ErrorNorm::RealT; + + /** + * @brief A workspace for the linear algebra operations required to calculate the norm. + * + */ + mutable struct + { + /** + * @brief The final vector which will have its norm taken. + * + */ + std::unique_ptr out_; + /** + * @brief The vector which will be used to scale the error in accordance with the tolerances. + * + */ + std::unique_ptr scale_; + /** + * @brief The absolute value of yprev. Used to calculate \ref scale_ + * + */ + std::unique_ptr yprev_abs_; + } workspace_; + + public: + /** + * @brief The configurable parameters of the error norm. + * + */ + struct Parameters + { + /** + * @brief A vector of absolute tolerances for each component. The norm will attempt to reject errors + * in each component above this tolerance. + * + */ + std::unique_ptr abs_tol_; + + /** + * @brief The relative tolerance. The norm will attempt to reject any error larger in percentage of + * the solution's maximum element than this. + * + */ + RealT rel_tol_; + } params_; + + InfNorm(Parameters&& params) + : params_(std::move(params)) + { + } + + RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const final; + }; + +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index ad3b38722..f213fe52d 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -944,85 +944,5 @@ namespace Integrator return 0; } - /** - * @brief Standard textbook adaptive controller. Accept if `err <= 1` and use - * - * \f[h_{new} = h * \min \left\{fac_{max}, \max\left\{fac_{min}, fac_{scale} \cdot e ^{-1/p}\right\}\right\}.\f] - * - */ - template - StepControl AdaptiveStep::nextStep(RealT err, StepControl prev_step, uint8_t method_order) - { - StepControl next_step = prev_step; - - double h_mult = std::min(params_.fac_max_, std::max(params_.fac_scale_ * std::pow(err, GridKit::MINUS_ONE / method_order), params_.fac_min_)); - - next_step.accept_ = err <= 1; - next_step.step_size_ *= h_mult; - - return next_step; - } - - /** - * @brief Fixed step - accept every step, no matter the error, and keep the step size the same. - * - */ - template - StepControl FixedStep::nextStep([[maybe_unused]] RealT err, StepControl prev_step, [[maybe_unused]] uint8_t method_order) - { - return StepControl{ - .accept_ = true, - .step_size_ = prev_step.step_size_, - }; - } - - /** - * @brief Calculate the infinity error norm as - * - * \f[e = \max_i\frac{|\hat{e}_i|}{Atol_i + Rtol \cdot \max\{|y_{0i}|, |y_{1i}|\}},\f] - * - * where \f(y_0\f) is the initial state, \f(y_1\f) is the next state, and \f(\hat{e}\f) is the estimated error made in calculating - * the next state (typically \f(\hat{e} = y_1 - \hat{y}_1\f) for some different-order approximation \f(\hat{y}_1\f)). - * - * @param err \f(\hat{e}\f) in the above formula. - * @param y \f(y_1\f) in the above formula. - * @param yprev \f(y_0\f) in the above formula. - * @param handler The handler to be used for performing linear algebra operations. - * @param memspace The memory space to be used for performing linear lagebra operations. - * @see `Rosenbrock::errorEstimate()` - */ - template - InfNorm::RealT InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const - { - if (int err_code = workspace_.out_->copyFromExternal(&err, memspace, memspace)) - { - throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); - } - if (int err_code = workspace_.scale_->copyFromExternal(&y, memspace, memspace)) - { - throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); - } - if (int err_code = workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace)) - { - throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); - } - - handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); - handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); - handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.scale_.get(), memspace); - - // TODO: This scal shouldn't be necessary, but axpy doesn't support scaling the y parameter. In the future, - // the scaling should be able to be put on the next axpy. - handler.scal(params_.rel_tol_, workspace_.scale_.get(), memspace); - handler.axpy(GridKit::ONE, params_.abs_tol_.get(), workspace_.scale_.get(), memspace); - handler.diagSolve(workspace_.scale_.get(), workspace_.out_.get(), memspace); - - return handler.amax(workspace_.out_.get(), memspace); - } - template class Rosenbrock; - - template class FixedStep; - - template class AdaptiveStep; } // namespace Integrator diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 3f36d6af0..34bd1f4b3 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -12,6 +12,9 @@ #include #include #include +#include +#include +#include #include #include @@ -20,81 +23,6 @@ namespace Integrator { - - /** - * @brief Define control flow for `StepController`s to be able to control the step size of a `Rosenbrock` integrator. - * - */ - template - struct StepControl - { - /** - * @brief Whether or not the step is accepted. A rejected step will cause the time step controller to discard - * the next state and re-step with the new `step_size`. - * - */ - bool accept_; - /** - * @brief The step size the next step should take. - * - */ - RealT step_size_; - }; - - /** - * @brief Interface for step size controllers. Used by `Rosenbrock` integrators to decide when to accept/reject steps and - * what size each step should be. - * - * @todo It may be best to have \ref usesError() return a reference to the \ref ErrorNorm that should be used. - */ - template - class StepController - { - public: - /** - * @brief Decide the control flow for the next step, based on information gathered by the integrator about the current step. - * - * @param err The estimated error made by the current step. Only calculated if `usesError()` returns true, otherwise it is assumed this - * value is not used. - * @param prev_step The control flow from before the current step was taken. Can be used to accurately update the step size. - * @param method_order The order of the method being used. - * @return StepControl - */ - virtual StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) = 0; - /** - * @brief Return whether or not the `nextStep` method implementation uses the `err` parameter. If `false`, this parameter is not calculated. - * - */ - virtual bool usesError() const = 0; - }; - - /** - * @brief Interface for error norms. Used to calculate the `err` parameter in `StepController::nextStep` based on a residual state error vector. - * - */ - template - class ErrorNorm - { - using State = GridKit::LinearAlgebra::Vector; - using RealT = typename GridKit::ScalarTraits::RealT; - - public: - /** - * @brief Calculate an error to be used by a step controller. Typically, an error > 1 indicates an error which does not meet tolerances, while - * an error < 1 indicates an error which meets tolerances. For that reason, tolerances should be included in the calculation of the error. - * - * @param err The state error residual being measured. - * @param y The state that the error was calculated from. Can be used for proper relative error normalization. - * @param yprev The state from the previous step. Can be used for proper relative error normalization. - * @param handler A vector handler which can be used to facilitate vector operations. - * @param memspace The memory space which vector operations should be performed in/. - * @return The error. - * - * @todo Allow this method to fail, since it will likely involve linear algebra calls. - */ - virtual RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const = 0; - }; - /** * @brief A Rosenbrock integrator designed to simulate a `GridKit::Model::Evaluator` model. * Rosenbrock integrators are best for quick medium-accuracy simulation and @@ -606,155 +534,4 @@ namespace Integrator [[nodiscard("May fail. Check error code.")]] int interpDense(RealT theta); }; - - /** - * @brief A simple textbook adaptive `StepController` which seeks to meet a relative and absolute tolerance - * based on an error estimate. - * - */ - template - class AdaptiveStep : public StepController - { - /** - * @brief Parameters for the step controller. - * - */ - struct Parameters - { - /** - * @brief The minimum multiple by which the step size can be multiplied to obtain the new step size. - * Increasing this can allow the integrator to be slightly more conservative in selecting the step size - * - decreasing the number of steps taken but increasing the risk of failing the next step. - * - * @note Should be between 0 and 1. - * - */ - RealT fac_min_ = 0.2; - /** - * @brief The maximum multiple by which the step size can be multiplied to obtain the new step size. - * Decreasing this will make the integrator more conservative in selecting the step size - - * increasing the number of steps taken but decreasing the risk of failing the next step. - * - * @note Should be greater than 1. - * - */ - RealT fac_max_ = 5.0; - /** - * @brief A "fudge factor" introduced to decrease risk of failing a step. The larger the fudge factor, - * the more likely steps will fail, but fewer steps will be taken. - * - * @note Should be between 0 and 1. - * - */ - RealT fac_scale_ = 0.9; - } params_; - - public: - AdaptiveStep(const Parameters& params) - : params_(params) - { - } - - StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; - - /** - * @brief This controller uses error estimates. - * - * @see `nextStep()` - * - */ - constexpr bool usesError() const final - { - return true; - } - }; - - /** - * @brief A fixed step controller which doesn't change the step size and accepts every step. - * Useful if you know what time scale your simulation operates on apriori and you're - * using a method without an embedded error controller. - * - * To set the fixed size, set the `Rosenbrock::Parameters::starting_step` parameter. - * - */ - template - class FixedStep : public StepController - { - StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; - - /** - * @brief This controller does not use error estimates. - * - * @see `nextStep()` - * - */ - constexpr bool usesError() const final - { - return false; - } - }; - - /** - * @brief The infinity error norm, which requires the error in every component in the system - * to meet tolerance. - * - */ - template - class InfNorm : public ErrorNorm - { - using State = GridKit::LinearAlgebra::Vector; - using RealT = ErrorNorm::RealT; - - /** - * @brief A workspace for the linear algebra operations required to calculate the norm. - * - */ - mutable struct - { - /** - * @brief The final vector which will have its norm taken. - * - */ - std::unique_ptr out_; - /** - * @brief The vector which will be used to scale the error in accordance with the tolerances. - * - */ - std::unique_ptr scale_; - /** - * @brief The absolute value of yprev. Used to calculate \ref scale_ - * - */ - std::unique_ptr yprev_abs_; - } workspace_; - - public: - /** - * @brief The configurable parameters of the error norm. - * - */ - struct Parameters - { - /** - * @brief A vector of absolute tolerances for each component. The norm will attempt to reject errors - * in each component above this tolerance. - * - */ - std::unique_ptr abs_tol_; - - /** - * @brief The relative tolerance. The norm will attempt to reject any error larger in percentage of - * the solution's maximum element than this. - * - */ - RealT rel_tol_; - } params_; - - InfNorm(Parameters&& params) - : params_(std::move(params)) - { - } - - RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const final; - }; } // namespace Integrator diff --git a/GridKit/Solver/Dynamic/StepControl.hpp b/GridKit/Solver/Dynamic/StepControl.hpp new file mode 100644 index 000000000..1d71c69f1 --- /dev/null +++ b/GridKit/Solver/Dynamic/StepControl.hpp @@ -0,0 +1,24 @@ +#pragma once + +namespace Integrator +{ + /** + * @brief Define control flow for `StepController`s to be able to control the step size of a `Rosenbrock` integrator. + * + */ + template + struct StepControl + { + /** + * @brief Whether or not the step is accepted. A rejected step will cause the time step controller to discard + * the next state and re-step with the new `step_size`. + * + */ + bool accept_; + /** + * @brief The step size the next step should take. + * + */ + RealT step_size_; + }; +} // namespace Integrator \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/StepController.hpp b/GridKit/Solver/Dynamic/StepController.hpp new file mode 100644 index 000000000..a685a4731 --- /dev/null +++ b/GridKit/Solver/Dynamic/StepController.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include + +namespace Integrator +{ + /** + * @brief Interface for step size controllers. Used by `Rosenbrock` integrators to decide when to accept/reject steps and + * what size each step should be. + * + * @todo It may be best to have \ref usesError() return a reference to the \ref ErrorNorm that should be used. + */ + template + class StepController + { + public: + /** + * @brief Decide the control flow for the next step, based on information gathered by the integrator about the current step. + * + * @param err The estimated error made by the current step. Only calculated if `usesError()` returns true, otherwise it is assumed this + * value is not used. + * @param prev_step The control flow from before the current step was taken. Can be used to accurately update the step size. + * @param method_order The order of the method being used. + * @return StepControl + */ + virtual StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) = 0; + /** + * @brief Return whether or not the `nextStep` method implementation uses the `err` parameter. If `false`, this parameter is not calculated. + * + */ + virtual bool usesError() const = 0; + }; +} // namespace Integrator \ No newline at end of file diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index 625736c49..1df78fa85 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -11,7 +11,7 @@ endif() if(GRIDKIT_ENABLE_RESOLVE) add_executable(test_rosenbrock runRosenbrockTests.cpp) - target_link_libraries(test_rosenbrock GridKit::rosenbrock GridKit::testing) + target_link_libraries(test_rosenbrock GridKit::rosenbrock GridKit::testing GridKit::fixed_step ReSolve::ReSolve) add_test(NAME RosenbrockTest COMMAND $) install(TARGETS test_rosenbrock RUNTIME DESTINATION bin) diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index 8a241314d..65ee5218b 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -4,12 +4,13 @@ #include #include +#include #include +#include #include #include #include -#include "GridKit/LinearAlgebra/SparseMatrix/CsrMatrix.hpp" #include #include #include From abd90710780ab0031a7fcf3edc64137cbcdff6e8 Mon Sep 17 00:00:00 2001 From: alexander-novo Date: Mon, 13 Jul 2026 15:56:20 +0000 Subject: [PATCH 47/57] Apply pre-commit fixes --- GridKit/Solver/Dynamic/AdaptiveStep.cpp | 2 +- GridKit/Solver/Dynamic/AdaptiveStep.hpp | 2 +- GridKit/Solver/Dynamic/CMakeLists.txt | 9 +++------ GridKit/Solver/Dynamic/ErrorNorm.hpp | 2 +- GridKit/Solver/Dynamic/FixedStep.cpp | 2 +- GridKit/Solver/Dynamic/FixedStep.hpp | 2 +- GridKit/Solver/Dynamic/InfNorm.cpp | 2 +- GridKit/Solver/Dynamic/InfNorm.hpp | 2 +- GridKit/Solver/Dynamic/StepControl.hpp | 2 +- GridKit/Solver/Dynamic/StepController.hpp | 2 +- tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 7 ++++++- 11 files changed, 18 insertions(+), 16 deletions(-) diff --git a/GridKit/Solver/Dynamic/AdaptiveStep.cpp b/GridKit/Solver/Dynamic/AdaptiveStep.cpp index 0e1db9ba0..0d6a4d7c1 100644 --- a/GridKit/Solver/Dynamic/AdaptiveStep.cpp +++ b/GridKit/Solver/Dynamic/AdaptiveStep.cpp @@ -26,4 +26,4 @@ namespace Integrator } template class AdaptiveStep; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/AdaptiveStep.hpp b/GridKit/Solver/Dynamic/AdaptiveStep.hpp index 246ab554e..16b61dc97 100644 --- a/GridKit/Solver/Dynamic/AdaptiveStep.hpp +++ b/GridKit/Solver/Dynamic/AdaptiveStep.hpp @@ -66,4 +66,4 @@ namespace Integrator return true; } }; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index c598fddff..fbcc105e4 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -66,18 +66,15 @@ if(GRIDKIT_ENABLE_RESOLVE) PRIVATE ReSolve::ReSolve PUBLIC - GridKit::dense_vector - ) + GridKit::dense_vector) endif() gridkit_add_library( adaptive_step SOURCES AdaptiveStep.cpp - HEADERS AdaptiveStep.hpp -) + HEADERS AdaptiveStep.hpp) gridkit_add_library( fixed_step SOURCES FixedStep.cpp - HEADERS FixedStep.hpp -) \ No newline at end of file + HEADERS FixedStep.hpp) diff --git a/GridKit/Solver/Dynamic/ErrorNorm.hpp b/GridKit/Solver/Dynamic/ErrorNorm.hpp index 3f5f88d8a..dde7ddbe7 100644 --- a/GridKit/Solver/Dynamic/ErrorNorm.hpp +++ b/GridKit/Solver/Dynamic/ErrorNorm.hpp @@ -32,4 +32,4 @@ namespace Integrator */ virtual RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const = 0; }; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/FixedStep.cpp b/GridKit/Solver/Dynamic/FixedStep.cpp index 68bbdc94c..87f366c53 100644 --- a/GridKit/Solver/Dynamic/FixedStep.cpp +++ b/GridKit/Solver/Dynamic/FixedStep.cpp @@ -16,4 +16,4 @@ namespace Integrator } template class FixedStep; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/FixedStep.hpp b/GridKit/Solver/Dynamic/FixedStep.hpp index bf02747c2..18f4bb624 100644 --- a/GridKit/Solver/Dynamic/FixedStep.hpp +++ b/GridKit/Solver/Dynamic/FixedStep.hpp @@ -28,4 +28,4 @@ namespace Integrator return false; } }; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/InfNorm.cpp b/GridKit/Solver/Dynamic/InfNorm.cpp index 069b18149..6ddff9204 100644 --- a/GridKit/Solver/Dynamic/InfNorm.cpp +++ b/GridKit/Solver/Dynamic/InfNorm.cpp @@ -49,4 +49,4 @@ namespace Integrator return handler.amax(workspace_.out_.get(), memspace); } -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/InfNorm.hpp b/GridKit/Solver/Dynamic/InfNorm.hpp index 2c4053a3a..42c85c608 100644 --- a/GridKit/Solver/Dynamic/InfNorm.hpp +++ b/GridKit/Solver/Dynamic/InfNorm.hpp @@ -72,4 +72,4 @@ namespace Integrator RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const final; }; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/StepControl.hpp b/GridKit/Solver/Dynamic/StepControl.hpp index 1d71c69f1..e48c71aa3 100644 --- a/GridKit/Solver/Dynamic/StepControl.hpp +++ b/GridKit/Solver/Dynamic/StepControl.hpp @@ -21,4 +21,4 @@ namespace Integrator */ RealT step_size_; }; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/GridKit/Solver/Dynamic/StepController.hpp b/GridKit/Solver/Dynamic/StepController.hpp index a685a4731..2013e72eb 100644 --- a/GridKit/Solver/Dynamic/StepController.hpp +++ b/GridKit/Solver/Dynamic/StepController.hpp @@ -32,4 +32,4 @@ namespace Integrator */ virtual bool usesError() const = 0; }; -} // namespace Integrator \ No newline at end of file +} // namespace Integrator diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index 1df78fa85..d2d9e9bb3 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -11,7 +11,12 @@ endif() if(GRIDKIT_ENABLE_RESOLVE) add_executable(test_rosenbrock runRosenbrockTests.cpp) - target_link_libraries(test_rosenbrock GridKit::rosenbrock GridKit::testing GridKit::fixed_step ReSolve::ReSolve) + target_link_libraries( + test_rosenbrock + GridKit::rosenbrock + GridKit::testing + GridKit::fixed_step + ReSolve::ReSolve) add_test(NAME RosenbrockTest COMMAND $) install(TARGETS test_rosenbrock RUNTIME DESTINATION bin) From 7c1662835bf985783d0b0efc84e5f71d6c1c489d Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Mon, 13 Jul 2026 16:11:57 -0400 Subject: [PATCH 48/57] Update Rosenbrock to use new LinearSolver interface --- GridKit/Solver/Dynamic/Rosenbrock.cpp | 52 +++---------------- GridKit/Solver/Dynamic/Rosenbrock.hpp | 29 ++--------- tests/UnitTests/Solver/Dynamic/CMakeLists.txt | 1 + .../Solver/Dynamic/RosenbrockTests.hpp | 11 ++-- 4 files changed, 18 insertions(+), 75 deletions(-) diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index f213fe52d..f0d2a7aee 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -5,6 +5,7 @@ #include #include +#include #include /** @@ -228,7 +229,7 @@ namespace Integrator template Rosenbrock::Rosenbrock(Tableau&& tab, GridKit::Model::Evaluator* model, - ReSolve::SystemSolver& lin_solver, + GridKit::LinearAlgebra::LinearSolver& lin_solver, GridKit::LinearAlgebra::VectorHandler& vector_handler, const ErrorNorm* err_norm, GridKit::memory::MemorySpace memspace) @@ -267,9 +268,6 @@ namespace Integrator workspace_.dFdt_ = std::make_unique(size); workspace_.mass_ = std::make_unique(size); - resolve_rhs_ = std::make_unique(size); - resolve_lhs_ = std::make_unique(size); - BUBBLE_FAIL(y_prev_->allocate(memspace_)); BUBBLE_FAIL(y_cur_->allocate(memspace_)); BUBBLE_FAIL(y_new_->allocate(memspace_)); @@ -309,8 +307,6 @@ namespace Integrator } } - workspace_.jacobian_ = std::make_unique(size, size, model_->getCsrJacobian()->getNnz()); - return 0; } @@ -339,19 +335,7 @@ namespace Integrator BUBBLE_FAIL(y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_)); workspace_.jacobian_analyzed_ = false; - GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); - BUBBLE_FAIL(workspace_.jacobian_->setDataPointers( - model_jacobian->getRowData(), - model_jacobian->getColData(), - model_jacobian->getValues(), - GridKit::memory::memorySpaceAsResolve(memspace_))); - BUBBLE_FAIL(lin_solver_.setMatrix(workspace_.jacobian_.get())); - BUBBLE_FAIL(lin_solver_.analyze()); - - // TODO: This function needs to be called to properly use a preconditioner in ReSolve (if there is any), but currently will error - // if there is no preconditioner configured. Once we can detect if a preconditioner is configured, we can restore this functionality. - // Also, we will always want to use *right* preconditioning. - // BUBBLE_FAIL(lin_solver_.preconditionerSetup("right")); + BUBBLE_FAIL(lin_solver_.configureSolver(*model_->getCsrJacobian())); if (model_->tag().size() != static_cast(model_->size())) { @@ -674,26 +658,8 @@ namespace Integrator // so we need a negative here since df/dy' = M. model_->updateTime(t0, MINUS_ONE / (dt * tab_.gamma_)); BUBBLE_FAIL(model_->evaluateJacobian()); - GridKit::LinearAlgebra::CsrMatrix* model_jacobian = model_->getCsrJacobian(); - - // TODO: This can likely be moved to allocate? These pointers should be consistent throughout the simulation - BUBBLE_FAIL(workspace_.jacobian_->setDataPointers( - model_jacobian->getRowData(), - model_jacobian->getColData(), - model_jacobian->getValues(), - GridKit::memory::memorySpaceAsResolve(memspace_))); - - // We must factorize first (slower) and then can re-factorize (faster) on later steps - [[likely]] - if (workspace_.jacobian_analyzed_) - { - BUBBLE_FAIL(lin_solver_.refactorize()); - } - else - { - BUBBLE_FAIL(lin_solver_.factorize()); - workspace_.jacobian_analyzed_ = true; - } + BUBBLE_FAIL(lin_solver_.setupSolver(workspace_.jacobian_analyzed_)); + workspace_.jacobian_analyzed_ = true; stats_.jac_evals_++; } @@ -719,9 +685,7 @@ namespace Integrator stats_.f_evals_++; } - resolve_rhs_->setData(workspace_.RHS_first_stage_->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); - resolve_lhs_->setData(workspace_.stages_[0]->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); - BUBBLE_FAIL(lin_solver_.solve(resolve_rhs_.get(), resolve_lhs_.get())); + BUBBLE_FAIL(lin_solver_.solve(*workspace_.RHS_first_stage_, *workspace_.stages_[0])); stats_.decomp_solves_++; // Rest of stages @@ -766,9 +730,7 @@ namespace Integrator vector_handler_.scal(workspace_.mass_.get(), workspace_.csum_.get(), memspace_); vector_handler_.axpy(MINUS_ONE, workspace_.csum_.get(), workspace_.RHS_.get(), memspace_); - resolve_rhs_->setData(workspace_.RHS_->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); - resolve_lhs_->setData(workspace_.stages_[i]->getData(), GridKit::memory::memorySpaceAsResolve(memspace_)); - BUBBLE_FAIL(lin_solver_.solve(resolve_rhs_.get(), resolve_lhs_.get())); + BUBBLE_FAIL(lin_solver_.solve(*workspace_.RHS_, *workspace_.stages_[i])); stats_.f_evals_++; stats_.decomp_solves_++; } diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 34bd1f4b3..87e061418 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -16,11 +17,6 @@ #include #include -#include -#include -#include -#include - namespace Integrator { /** @@ -348,7 +344,7 @@ namespace Integrator * @brief The linear solver to be used during integration in \link timeStep() \endlink. * */ - ReSolve::SystemSolver& lin_solver_; + GridKit::LinearAlgebra::LinearSolver& lin_solver_; /** * @brief The vector handler to be used for vector operations by the integrator. * @@ -395,19 +391,6 @@ namespace Integrator */ std::unique_ptr y_interp_; - /** - * @brief Re::Solve vector used for the right hand side of a linear solve. No allocation is done - - * the vector simply has its data pointer updated to point at the correct GridKit vector before the - * solve operation is called. - * - */ - std::unique_ptr resolve_rhs_; - /** - * @brief Re::Solve vector used for the left hand side of a linear solve. \see resolve_rhs_ - * - */ - std::unique_ptr resolve_lhs_; - /** * @brief Configured parameters for the integrator. * @@ -423,7 +406,7 @@ namespace Integrator public: Rosenbrock(Tableau&& tab, GridKit::Model::Evaluator* model, - ReSolve::SystemSolver& lin_solver, + GridKit::LinearAlgebra::LinearSolver& lin_solver, GridKit::LinearAlgebra::VectorHandler& vector_handler, const ErrorNorm* err_norm, GridKit::memory::MemorySpace memspace = GridKit::memory::HOST); @@ -489,12 +472,6 @@ namespace Integrator */ std::unique_ptr err_est_; - /** - * @brief Jacobian matrix - * - */ - std::unique_ptr jacobian_; - /** * @brief Whether or not the Jacobian has been factorized. Initial factorization is required, * but pivots can be re-used for a refactorization later. Re-factorization can introduce diff --git a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt index d2d9e9bb3..35d1f70e2 100644 --- a/tests/UnitTests/Solver/Dynamic/CMakeLists.txt +++ b/tests/UnitTests/Solver/Dynamic/CMakeLists.txt @@ -16,6 +16,7 @@ if(GRIDKIT_ENABLE_RESOLVE) GridKit::rosenbrock GridKit::testing GridKit::fixed_step + GridKit::resolve_system_solver ReSolve::ReSolve) add_test(NAME RosenbrockTest COMMAND $) diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index 65ee5218b..8d3ff9d14 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -11,6 +12,7 @@ #include #include +#include #include #include #include @@ -347,11 +349,12 @@ namespace GridKit model.allocate(); model.initialize(); - ReSolve::LinAlgWorkspaceCpu linear_workspace; - ReSolve::SystemSolver lin_solver(&linear_workspace, "klu", "klu", "klu"); - GridKit::LinearAlgebra::VectorHandler vec_handler; + ReSolve::LinAlgWorkspaceCpu linear_workspace; + ReSolve::SystemSolver resolve_solver(&linear_workspace, "klu", "klu", "klu"); + GridKit::LinearAlgebra::VectorHandler vec_handler; + GridKit::LinearAlgebra::ResolveSystemSolver lin_solver(resolve_solver); - lin_solver.initialize(); + resolve_solver.initialize(); Rosenbrock integrator(std::move(tab), &model, lin_solver, vec_handler, nullptr); if (integrator.allocate()) From a69598f5fa45583ebce214b8ee8708e2700e4b0a Mon Sep 17 00:00:00 2001 From: alexander-novo Date: Mon, 13 Jul 2026 20:32:35 +0000 Subject: [PATCH 49/57] Apply pre-commit fixes --- GridKit/LinearAlgebra/CMakeLists.txt | 2 +- GridKit/LinearAlgebra/Solver/CMakeLists.txt | 7 ++----- GridKit/LinearAlgebra/Solver/ResolveSystemSolver.cpp | 5 +++-- GridKit/LinearAlgebra/Solver/ResolveSystemSolver.hpp | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/GridKit/LinearAlgebra/CMakeLists.txt b/GridKit/LinearAlgebra/CMakeLists.txt index 0fe740023..a28565ed1 100644 --- a/GridKit/LinearAlgebra/CMakeLists.txt +++ b/GridKit/LinearAlgebra/CMakeLists.txt @@ -1,4 +1,4 @@ add_subdirectory(SparseMatrix) add_subdirectory(DenseMatrix) add_subdirectory(Vector) -add_subdirectory(Solver) \ No newline at end of file +add_subdirectory(Solver) diff --git a/GridKit/LinearAlgebra/Solver/CMakeLists.txt b/GridKit/LinearAlgebra/Solver/CMakeLists.txt index 356cc12e4..51da2bb1b 100644 --- a/GridKit/LinearAlgebra/Solver/CMakeLists.txt +++ b/GridKit/LinearAlgebra/Solver/CMakeLists.txt @@ -3,8 +3,5 @@ if(GRIDKIT_ENABLE_RESOLVE) resolve_system_solver SOURCES ResolveSystemSolver.cpp HEADERS ResolveSystemSolver.hpp - LINK_LIBRARIES - PRIVATE - ReSolve::ReSolve - ) -endif() \ No newline at end of file + LINK_LIBRARIES PRIVATE ReSolve::ReSolve) +endif() diff --git a/GridKit/LinearAlgebra/Solver/ResolveSystemSolver.cpp b/GridKit/LinearAlgebra/Solver/ResolveSystemSolver.cpp index 127a6119b..4d6797147 100644 --- a/GridKit/LinearAlgebra/Solver/ResolveSystemSolver.cpp +++ b/GridKit/LinearAlgebra/Solver/ResolveSystemSolver.cpp @@ -25,7 +25,8 @@ namespace GridKit } template - ResolveSystemSolver::ResolveSystemSolver(ReSolve::SystemSolver& lin_solver, GridKit::memory::MemorySpace memspace) : lin_solver_(lin_solver), memspace_(memorySpaceAsResolve(memspace)) + ResolveSystemSolver::ResolveSystemSolver(ReSolve::SystemSolver& lin_solver, GridKit::memory::MemorySpace memspace) + : lin_solver_(lin_solver), memspace_(memorySpaceAsResolve(memspace)) { } @@ -101,4 +102,4 @@ namespace GridKit template class ResolveSystemSolver; } // namespace LinearAlgebra -} // namespace GridKit \ No newline at end of file +} // namespace GridKit diff --git a/GridKit/LinearAlgebra/Solver/ResolveSystemSolver.hpp b/GridKit/LinearAlgebra/Solver/ResolveSystemSolver.hpp index f666f9b4d..9ca55ef19 100644 --- a/GridKit/LinearAlgebra/Solver/ResolveSystemSolver.hpp +++ b/GridKit/LinearAlgebra/Solver/ResolveSystemSolver.hpp @@ -42,4 +42,4 @@ namespace GridKit ReSolve::memory::MemorySpace memspace_; }; } // namespace LinearAlgebra -} // namespace GridKit \ No newline at end of file +} // namespace GridKit From 8cd2a684e0a5b928456293f7bad5f311ecd2e5ba Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 14 Jul 2026 13:49:20 -0400 Subject: [PATCH 50/57] Moved Rosenbrock support classes to Native folder --- GridKit/Solver/Dynamic/CMakeLists.txt | 20 +----------------- .../Dynamic/{ => Native}/AdaptiveStep.cpp | 0 .../Dynamic/{ => Native}/AdaptiveStep.hpp | 2 +- GridKit/Solver/Dynamic/Native/CMakeLists.txt | 21 +++++++++++++++++++ .../Solver/Dynamic/{ => Native}/ErrorNorm.hpp | 0 .../Solver/Dynamic/{ => Native}/FixedStep.cpp | 0 .../Solver/Dynamic/{ => Native}/FixedStep.hpp | 2 +- .../Solver/Dynamic/{ => Native}/InfNorm.cpp | 0 .../Solver/Dynamic/{ => Native}/InfNorm.hpp | 2 +- .../Dynamic/{ => Native}/StepControl.hpp | 0 .../Dynamic/{ => Native}/StepController.hpp | 2 +- GridKit/Solver/Dynamic/Rosenbrock.hpp | 4 ++-- .../Solver/Dynamic/RosenbrockTests.hpp | 2 +- 13 files changed, 29 insertions(+), 26 deletions(-) rename GridKit/Solver/Dynamic/{ => Native}/AdaptiveStep.cpp (100%) rename GridKit/Solver/Dynamic/{ => Native}/AdaptiveStep.hpp (97%) create mode 100644 GridKit/Solver/Dynamic/Native/CMakeLists.txt rename GridKit/Solver/Dynamic/{ => Native}/ErrorNorm.hpp (100%) rename GridKit/Solver/Dynamic/{ => Native}/FixedStep.cpp (100%) rename GridKit/Solver/Dynamic/{ => Native}/FixedStep.hpp (93%) rename GridKit/Solver/Dynamic/{ => Native}/InfNorm.cpp (100%) rename GridKit/Solver/Dynamic/{ => Native}/InfNorm.hpp (97%) rename GridKit/Solver/Dynamic/{ => Native}/StepControl.hpp (100%) rename GridKit/Solver/Dynamic/{ => Native}/StepController.hpp (96%) diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index fbcc105e4..dd4e87268 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -57,24 +57,6 @@ if(GRIDKIT_ENABLE_RESOLVE) GridKit::utilities_logger PUBLIC GridKit::sparse_matrix) - - gridkit_add_library( - inf_norm - SOURCES InfNorm.cpp - HEADERS InfNorm.hpp - LINK_LIBRARIES - PRIVATE - ReSolve::ReSolve - PUBLIC - GridKit::dense_vector) endif() -gridkit_add_library( - adaptive_step - SOURCES AdaptiveStep.cpp - HEADERS AdaptiveStep.hpp) - -gridkit_add_library( - fixed_step - SOURCES FixedStep.cpp - HEADERS FixedStep.hpp) +add_subdirectory(Native) \ No newline at end of file diff --git a/GridKit/Solver/Dynamic/AdaptiveStep.cpp b/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp similarity index 100% rename from GridKit/Solver/Dynamic/AdaptiveStep.cpp rename to GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp diff --git a/GridKit/Solver/Dynamic/AdaptiveStep.hpp b/GridKit/Solver/Dynamic/Native/AdaptiveStep.hpp similarity index 97% rename from GridKit/Solver/Dynamic/AdaptiveStep.hpp rename to GridKit/Solver/Dynamic/Native/AdaptiveStep.hpp index 16b61dc97..4e0d7db7e 100644 --- a/GridKit/Solver/Dynamic/AdaptiveStep.hpp +++ b/GridKit/Solver/Dynamic/Native/AdaptiveStep.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace Integrator { diff --git a/GridKit/Solver/Dynamic/Native/CMakeLists.txt b/GridKit/Solver/Dynamic/Native/CMakeLists.txt new file mode 100644 index 000000000..d42b58eb5 --- /dev/null +++ b/GridKit/Solver/Dynamic/Native/CMakeLists.txt @@ -0,0 +1,21 @@ +if(GRIDKIT_ENABLE_RESOLVE) + gridkit_add_library( + inf_norm + SOURCES InfNorm.cpp + HEADERS InfNorm.hpp + LINK_LIBRARIES + PRIVATE + ReSolve::ReSolve + PUBLIC + GridKit::dense_vector) +endif() + +gridkit_add_library( + adaptive_step + SOURCES AdaptiveStep.cpp + HEADERS AdaptiveStep.hpp) + +gridkit_add_library( + fixed_step + SOURCES FixedStep.cpp + HEADERS FixedStep.hpp) diff --git a/GridKit/Solver/Dynamic/ErrorNorm.hpp b/GridKit/Solver/Dynamic/Native/ErrorNorm.hpp similarity index 100% rename from GridKit/Solver/Dynamic/ErrorNorm.hpp rename to GridKit/Solver/Dynamic/Native/ErrorNorm.hpp diff --git a/GridKit/Solver/Dynamic/FixedStep.cpp b/GridKit/Solver/Dynamic/Native/FixedStep.cpp similarity index 100% rename from GridKit/Solver/Dynamic/FixedStep.cpp rename to GridKit/Solver/Dynamic/Native/FixedStep.cpp diff --git a/GridKit/Solver/Dynamic/FixedStep.hpp b/GridKit/Solver/Dynamic/Native/FixedStep.hpp similarity index 93% rename from GridKit/Solver/Dynamic/FixedStep.hpp rename to GridKit/Solver/Dynamic/Native/FixedStep.hpp index 18f4bb624..11a01c007 100644 --- a/GridKit/Solver/Dynamic/FixedStep.hpp +++ b/GridKit/Solver/Dynamic/Native/FixedStep.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace Integrator { diff --git a/GridKit/Solver/Dynamic/InfNorm.cpp b/GridKit/Solver/Dynamic/Native/InfNorm.cpp similarity index 100% rename from GridKit/Solver/Dynamic/InfNorm.cpp rename to GridKit/Solver/Dynamic/Native/InfNorm.cpp diff --git a/GridKit/Solver/Dynamic/InfNorm.hpp b/GridKit/Solver/Dynamic/Native/InfNorm.hpp similarity index 97% rename from GridKit/Solver/Dynamic/InfNorm.hpp rename to GridKit/Solver/Dynamic/Native/InfNorm.hpp index 42c85c608..28910e1a5 100644 --- a/GridKit/Solver/Dynamic/InfNorm.hpp +++ b/GridKit/Solver/Dynamic/Native/InfNorm.hpp @@ -3,7 +3,7 @@ #include #include -#include +#include namespace Integrator { diff --git a/GridKit/Solver/Dynamic/StepControl.hpp b/GridKit/Solver/Dynamic/Native/StepControl.hpp similarity index 100% rename from GridKit/Solver/Dynamic/StepControl.hpp rename to GridKit/Solver/Dynamic/Native/StepControl.hpp diff --git a/GridKit/Solver/Dynamic/StepController.hpp b/GridKit/Solver/Dynamic/Native/StepController.hpp similarity index 96% rename from GridKit/Solver/Dynamic/StepController.hpp rename to GridKit/Solver/Dynamic/Native/StepController.hpp index 2013e72eb..c3f5df739 100644 --- a/GridKit/Solver/Dynamic/StepController.hpp +++ b/GridKit/Solver/Dynamic/Native/StepController.hpp @@ -2,7 +2,7 @@ #include -#include +#include namespace Integrator { diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index 87e061418..d9a91986a 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -14,8 +14,8 @@ #include #include #include -#include -#include +#include +#include namespace Integrator { diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index 8d3ff9d14..6d464732d 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include From e5226df7fdfd1a6e9ef700cab2a4573b60694465 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 14 Jul 2026 14:02:41 -0400 Subject: [PATCH 51/57] Change Integrator namespace --- .../Solver/Dynamic/Native/AdaptiveStep.cpp | 37 +- .../Solver/Dynamic/Native/AdaptiveStep.hpp | 107 +- GridKit/Solver/Dynamic/Native/ErrorNorm.hpp | 51 +- GridKit/Solver/Dynamic/Native/FixedStep.cpp | 31 +- GridKit/Solver/Dynamic/Native/FixedStep.hpp | 43 +- GridKit/Solver/Dynamic/Native/InfNorm.cpp | 83 +- GridKit/Solver/Dynamic/Native/InfNorm.hpp | 103 +- GridKit/Solver/Dynamic/Native/StepControl.hpp | 37 +- .../Solver/Dynamic/Native/StepController.hpp | 51 +- GridKit/Solver/Dynamic/Rosenbrock.cpp | 1507 +++++++++-------- GridKit/Solver/Dynamic/Rosenbrock.hpp | 835 ++++----- GridKit/Solver/Dynamic/RosenbrockTableaus.cpp | 399 ++--- .../Solver/Dynamic/RosenbrockTests.hpp | 4 +- .../Solver/Dynamic/runRosenbrockTests.cpp | 5 +- 14 files changed, 1665 insertions(+), 1628 deletions(-) diff --git a/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp b/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp index 0d6a4d7c1..003bb8281 100644 --- a/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp +++ b/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp @@ -4,26 +4,29 @@ #include -namespace Integrator +namespace AnalysisManager { - /** - * @brief Standard textbook adaptive controller. Accept if `err <= 1` and use - * - * \f[h_{new} = h * \min \left\{fac_{max}, \max\left\{fac_{min}, fac_{scale} \cdot e ^{-1/p}\right\}\right\}.\f] - * - */ - template - StepControl AdaptiveStep::nextStep(RealT err, StepControl prev_step, uint8_t method_order) + namespace NativeDynamicSolver { - StepControl next_step = prev_step; + /** + * @brief Standard textbook adaptive controller. Accept if `err <= 1` and use + * + * \f[h_{new} = h * \min \left\{fac_{max}, \max\left\{fac_{min}, fac_{scale} \cdot e ^{-1/p}\right\}\right\}.\f] + * + */ + template + StepControl AdaptiveStep::nextStep(RealT err, StepControl prev_step, uint8_t method_order) + { + StepControl next_step = prev_step; - double h_mult = std::min(params_.fac_max_, std::max(params_.fac_scale_ * std::pow(err, GridKit::MINUS_ONE / method_order), params_.fac_min_)); + double h_mult = std::min(params_.fac_max_, std::max(params_.fac_scale_ * std::pow(err, GridKit::MINUS_ONE / method_order), params_.fac_min_)); - next_step.accept_ = err <= 1; - next_step.step_size_ *= h_mult; + next_step.accept_ = err <= 1; + next_step.step_size_ *= h_mult; - return next_step; - } + return next_step; + } - template class AdaptiveStep; -} // namespace Integrator + template class AdaptiveStep; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Native/AdaptiveStep.hpp b/GridKit/Solver/Dynamic/Native/AdaptiveStep.hpp index 4e0d7db7e..11740696b 100644 --- a/GridKit/Solver/Dynamic/Native/AdaptiveStep.hpp +++ b/GridKit/Solver/Dynamic/Native/AdaptiveStep.hpp @@ -2,68 +2,71 @@ #include -namespace Integrator +namespace AnalysisManager { - - /** - * @brief A simple textbook adaptive `StepController` which seeks to meet a relative and absolute tolerance - * based on an error estimate. - * - */ - template - class AdaptiveStep : public StepController + namespace NativeDynamicSolver { + /** - * @brief Parameters for the step controller. + * @brief A simple textbook adaptive `StepController` which seeks to meet a relative and absolute tolerance + * based on an error estimate. * */ - struct Parameters + template + class AdaptiveStep : public StepController { /** - * @brief The minimum multiple by which the step size can be multiplied to obtain the new step size. - * Increasing this can allow the integrator to be slightly more conservative in selecting the step size - * - decreasing the number of steps taken but increasing the risk of failing the next step. - * - * @note Should be between 0 and 1. - * - */ - RealT fac_min_ = 0.2; - /** - * @brief The maximum multiple by which the step size can be multiplied to obtain the new step size. - * Decreasing this will make the integrator more conservative in selecting the step size - - * increasing the number of steps taken but decreasing the risk of failing the next step. - * - * @note Should be greater than 1. + * @brief Parameters for the step controller. * */ - RealT fac_max_ = 5.0; + struct Parameters + { + /** + * @brief The minimum multiple by which the step size can be multiplied to obtain the new step size. + * Increasing this can allow the integrator to be slightly more conservative in selecting the step size + * - decreasing the number of steps taken but increasing the risk of failing the next step. + * + * @note Should be between 0 and 1. + * + */ + RealT fac_min_ = 0.2; + /** + * @brief The maximum multiple by which the step size can be multiplied to obtain the new step size. + * Decreasing this will make the integrator more conservative in selecting the step size - + * increasing the number of steps taken but decreasing the risk of failing the next step. + * + * @note Should be greater than 1. + * + */ + RealT fac_max_ = 5.0; + /** + * @brief A "fudge factor" introduced to decrease risk of failing a step. The larger the fudge factor, + * the more likely steps will fail, but fewer steps will be taken. + * + * @note Should be between 0 and 1. + * + */ + RealT fac_scale_ = 0.9; + } params_; + + public: + AdaptiveStep(const Parameters& params) + : params_(params) + { + } + + StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; + /** - * @brief A "fudge factor" introduced to decrease risk of failing a step. The larger the fudge factor, - * the more likely steps will fail, but fewer steps will be taken. + * @brief This controller uses error estimates. * - * @note Should be between 0 and 1. + * @see `nextStep()` * */ - RealT fac_scale_ = 0.9; - } params_; - - public: - AdaptiveStep(const Parameters& params) - : params_(params) - { - } - - StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; - - /** - * @brief This controller uses error estimates. - * - * @see `nextStep()` - * - */ - constexpr bool usesError() const final - { - return true; - } - }; -} // namespace Integrator + constexpr bool usesError() const final + { + return true; + } + }; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Native/ErrorNorm.hpp b/GridKit/Solver/Dynamic/Native/ErrorNorm.hpp index dde7ddbe7..279e9255f 100644 --- a/GridKit/Solver/Dynamic/Native/ErrorNorm.hpp +++ b/GridKit/Solver/Dynamic/Native/ErrorNorm.hpp @@ -4,32 +4,35 @@ #include #include -namespace Integrator +namespace AnalysisManager { - /** - * @brief Interface for error norms. Used to calculate the `err` parameter in `StepController::nextStep` based on a residual state error vector. - * - */ - template - class ErrorNorm + namespace NativeDynamicSolver { - using State = GridKit::LinearAlgebra::Vector; - using RealT = typename GridKit::ScalarTraits::RealT; - - public: /** - * @brief Calculate an error to be used by a step controller. Typically, an error > 1 indicates an error which does not meet tolerances, while - * an error < 1 indicates an error which meets tolerances. For that reason, tolerances should be included in the calculation of the error. - * - * @param err The state error residual being measured. - * @param y The state that the error was calculated from. Can be used for proper relative error normalization. - * @param yprev The state from the previous step. Can be used for proper relative error normalization. - * @param handler A vector handler which can be used to facilitate vector operations. - * @param memspace The memory space which vector operations should be performed in/. - * @return The error. + * @brief Interface for error norms. Used to calculate the `err` parameter in `StepController::nextStep` based on a residual state error vector. * - * @todo Allow this method to fail, since it will likely involve linear algebra calls. */ - virtual RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const = 0; - }; -} // namespace Integrator + template + class ErrorNorm + { + using State = GridKit::LinearAlgebra::Vector; + using RealT = typename GridKit::ScalarTraits::RealT; + + public: + /** + * @brief Calculate an error to be used by a step controller. Typically, an error > 1 indicates an error which does not meet tolerances, while + * an error < 1 indicates an error which meets tolerances. For that reason, tolerances should be included in the calculation of the error. + * + * @param err The state error residual being measured. + * @param y The state that the error was calculated from. Can be used for proper relative error normalization. + * @param yprev The state from the previous step. Can be used for proper relative error normalization. + * @param handler A vector handler which can be used to facilitate vector operations. + * @param memspace The memory space which vector operations should be performed in/. + * @return The error. + * + * @todo Allow this method to fail, since it will likely involve linear algebra calls. + */ + virtual RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const = 0; + }; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Native/FixedStep.cpp b/GridKit/Solver/Dynamic/Native/FixedStep.cpp index 87f366c53..965627ac0 100644 --- a/GridKit/Solver/Dynamic/Native/FixedStep.cpp +++ b/GridKit/Solver/Dynamic/Native/FixedStep.cpp @@ -1,19 +1,22 @@ #include "FixedStep.hpp" -namespace Integrator +namespace AnalysisManager { - /** - * @brief Fixed step - accept every step, no matter the error, and keep the step size the same. - * - */ - template - StepControl FixedStep::nextStep([[maybe_unused]] RealT err, StepControl prev_step, [[maybe_unused]] uint8_t method_order) + namespace NativeDynamicSolver { - return StepControl{ - .accept_ = true, - .step_size_ = prev_step.step_size_, - }; - } + /** + * @brief Fixed step - accept every step, no matter the error, and keep the step size the same. + * + */ + template + StepControl FixedStep::nextStep([[maybe_unused]] RealT err, StepControl prev_step, [[maybe_unused]] uint8_t method_order) + { + return StepControl{ + .accept_ = true, + .step_size_ = prev_step.step_size_, + }; + } - template class FixedStep; -} // namespace Integrator + template class FixedStep; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Native/FixedStep.hpp b/GridKit/Solver/Dynamic/Native/FixedStep.hpp index 11a01c007..82edda6cf 100644 --- a/GridKit/Solver/Dynamic/Native/FixedStep.hpp +++ b/GridKit/Solver/Dynamic/Native/FixedStep.hpp @@ -2,30 +2,33 @@ #include -namespace Integrator +namespace AnalysisManager { - /** - * @brief A fixed step controller which doesn't change the step size and accepts every step. - * Useful if you know what time scale your simulation operates on apriori and you're - * using a method without an embedded error controller. - * - * To set the fixed size, set the `Rosenbrock::Parameters::starting_step` parameter. - * - */ - template - class FixedStep : public StepController + namespace NativeDynamicSolver { - StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; - /** - * @brief This controller does not use error estimates. + * @brief A fixed step controller which doesn't change the step size and accepts every step. + * Useful if you know what time scale your simulation operates on apriori and you're + * using a method without an embedded error controller. * - * @see `nextStep()` + * To set the fixed size, set the `Rosenbrock::Parameters::starting_step` parameter. * */ - constexpr bool usesError() const final + template + class FixedStep : public StepController { - return false; - } - }; -} // namespace Integrator + StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) final; + + /** + * @brief This controller does not use error estimates. + * + * @see `nextStep()` + * + */ + constexpr bool usesError() const final + { + return false; + } + }; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Native/InfNorm.cpp b/GridKit/Solver/Dynamic/Native/InfNorm.cpp index 6ddff9204..f1edc419e 100644 --- a/GridKit/Solver/Dynamic/Native/InfNorm.cpp +++ b/GridKit/Solver/Dynamic/Native/InfNorm.cpp @@ -4,49 +4,52 @@ #include -namespace Integrator +namespace AnalysisManager { - /** - * @brief Calculate the infinity error norm as - * - * \f[e = \max_i\frac{|\hat{e}_i|}{Atol_i + Rtol \cdot \max\{|y_{0i}|, |y_{1i}|\}},\f] - * - * where \f(y_0\f) is the initial state, \f(y_1\f) is the next state, and \f(\hat{e}\f) is the estimated error made in calculating - * the next state (typically \f(\hat{e} = y_1 - \hat{y}_1\f) for some different-order approximation \f(\hat{y}_1\f)). - * - * @param err \f(\hat{e}\f) in the above formula. - * @param y \f(y_1\f) in the above formula. - * @param yprev \f(y_0\f) in the above formula. - * @param handler The handler to be used for performing linear algebra operations. - * @param memspace The memory space to be used for performing linear lagebra operations. - * @see `Rosenbrock::errorEstimate()` - */ - template - InfNorm::RealT InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const + namespace NativeDynamicSolver { - if (int err_code = workspace_.out_->copyFromExternal(&err, memspace, memspace)) + /** + * @brief Calculate the infinity error norm as + * + * \f[e = \max_i\frac{|\hat{e}_i|}{Atol_i + Rtol \cdot \max\{|y_{0i}|, |y_{1i}|\}},\f] + * + * where \f(y_0\f) is the initial state, \f(y_1\f) is the next state, and \f(\hat{e}\f) is the estimated error made in calculating + * the next state (typically \f(\hat{e} = y_1 - \hat{y}_1\f) for some different-order approximation \f(\hat{y}_1\f)). + * + * @param err \f(\hat{e}\f) in the above formula. + * @param y \f(y_1\f) in the above formula. + * @param yprev \f(y_0\f) in the above formula. + * @param handler The handler to be used for performing linear algebra operations. + * @param memspace The memory space to be used for performing linear lagebra operations. + * @see `Rosenbrock::errorEstimate()` + */ + template + InfNorm::RealT InfNorm::errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const { - throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); - } - if (int err_code = workspace_.scale_->copyFromExternal(&y, memspace, memspace)) - { - throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); - } - if (int err_code = workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace)) - { - throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); - } + if (int err_code = workspace_.out_->copyFromExternal(&err, memspace, memspace)) + { + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); + } + if (int err_code = workspace_.scale_->copyFromExternal(&y, memspace, memspace)) + { + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); + } + if (int err_code = workspace_.yprev_abs_->copyFromExternal(&yprev, memspace, memspace)) + { + throw std::format("GridKit::LinearAlgebra::Vector::copyFromExternal failed with error code {}", err_code); + } - handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); - handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); - handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.scale_.get(), memspace); + handler.abs(workspace_.scale_.get(), workspace_.scale_.get(), memspace); + handler.abs(workspace_.yprev_abs_.get(), workspace_.scale_.get(), memspace); + handler.max(workspace_.yprev_abs_.get(), workspace_.scale_.get(), workspace_.scale_.get(), memspace); - // TODO: This scal shouldn't be necessary, but axpy doesn't support scaling the y parameter. In the future, - // the scaling should be able to be put on the next axpy. - handler.scal(params_.rel_tol_, workspace_.scale_.get(), memspace); - handler.axpy(GridKit::ONE, params_.abs_tol_.get(), workspace_.scale_.get(), memspace); - handler.diagSolve(workspace_.scale_.get(), workspace_.out_.get(), memspace); + // TODO: This scal shouldn't be necessary, but axpy doesn't support scaling the y parameter. In the future, + // the scaling should be able to be put on the next axpy. + handler.scal(params_.rel_tol_, workspace_.scale_.get(), memspace); + handler.axpy(GridKit::ONE, params_.abs_tol_.get(), workspace_.scale_.get(), memspace); + handler.diagSolve(workspace_.scale_.get(), workspace_.out_.get(), memspace); - return handler.amax(workspace_.out_.get(), memspace); - } -} // namespace Integrator + return handler.amax(workspace_.out_.get(), memspace); + } + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Native/InfNorm.hpp b/GridKit/Solver/Dynamic/Native/InfNorm.hpp index 28910e1a5..6cc7a39a3 100644 --- a/GridKit/Solver/Dynamic/Native/InfNorm.hpp +++ b/GridKit/Solver/Dynamic/Native/InfNorm.hpp @@ -5,71 +5,74 @@ #include #include -namespace Integrator +namespace AnalysisManager { - - /** - * @brief The infinity error norm, which requires the error in every component in the system - * to meet tolerance. - * - */ - template - class InfNorm : public ErrorNorm + namespace NativeDynamicSolver { - using State = GridKit::LinearAlgebra::Vector; - using RealT = ErrorNorm::RealT; /** - * @brief A workspace for the linear algebra operations required to calculate the norm. + * @brief The infinity error norm, which requires the error in every component in the system + * to meet tolerance. * */ - mutable struct + template + class InfNorm : public ErrorNorm { - /** - * @brief The final vector which will have its norm taken. - * - */ - std::unique_ptr out_; - /** - * @brief The vector which will be used to scale the error in accordance with the tolerances. - * - */ - std::unique_ptr scale_; - /** - * @brief The absolute value of yprev. Used to calculate \ref scale_ - * - */ - std::unique_ptr yprev_abs_; - } workspace_; + using State = GridKit::LinearAlgebra::Vector; + using RealT = ErrorNorm::RealT; - public: - /** - * @brief The configurable parameters of the error norm. - * - */ - struct Parameters - { /** - * @brief A vector of absolute tolerances for each component. The norm will attempt to reject errors - * in each component above this tolerance. + * @brief A workspace for the linear algebra operations required to calculate the norm. * */ - std::unique_ptr abs_tol_; + mutable struct + { + /** + * @brief The final vector which will have its norm taken. + * + */ + std::unique_ptr out_; + /** + * @brief The vector which will be used to scale the error in accordance with the tolerances. + * + */ + std::unique_ptr scale_; + /** + * @brief The absolute value of yprev. Used to calculate \ref scale_ + * + */ + std::unique_ptr yprev_abs_; + } workspace_; + public: /** - * @brief The relative tolerance. The norm will attempt to reject any error larger in percentage of - * the solution's maximum element than this. + * @brief The configurable parameters of the error norm. * */ - RealT rel_tol_; - } params_; + struct Parameters + { + /** + * @brief A vector of absolute tolerances for each component. The norm will attempt to reject errors + * in each component above this tolerance. + * + */ + std::unique_ptr abs_tol_; - InfNorm(Parameters&& params) - : params_(std::move(params)) - { - } + /** + * @brief The relative tolerance. The norm will attempt to reject any error larger in percentage of + * the solution's maximum element than this. + * + */ + RealT rel_tol_; + } params_; + + InfNorm(Parameters&& params) + : params_(std::move(params)) + { + } - RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const final; - }; + RealT errorNorm(State& err, State& y, State& yprev, GridKit::LinearAlgebra::VectorHandler& handler, GridKit::memory::MemorySpace memspace) const final; + }; -} // namespace Integrator + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Native/StepControl.hpp b/GridKit/Solver/Dynamic/Native/StepControl.hpp index e48c71aa3..5dc678a4c 100644 --- a/GridKit/Solver/Dynamic/Native/StepControl.hpp +++ b/GridKit/Solver/Dynamic/Native/StepControl.hpp @@ -1,24 +1,27 @@ #pragma once -namespace Integrator +namespace AnalysisManager { - /** - * @brief Define control flow for `StepController`s to be able to control the step size of a `Rosenbrock` integrator. - * - */ - template - struct StepControl + namespace NativeDynamicSolver { /** - * @brief Whether or not the step is accepted. A rejected step will cause the time step controller to discard - * the next state and re-step with the new `step_size`. + * @brief Define control flow for `StepController`s to be able to control the step size of a `Rosenbrock` integrator. * */ - bool accept_; - /** - * @brief The step size the next step should take. - * - */ - RealT step_size_; - }; -} // namespace Integrator + template + struct StepControl + { + /** + * @brief Whether or not the step is accepted. A rejected step will cause the time step controller to discard + * the next state and re-step with the new `step_size`. + * + */ + bool accept_; + /** + * @brief The step size the next step should take. + * + */ + RealT step_size_; + }; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Native/StepController.hpp b/GridKit/Solver/Dynamic/Native/StepController.hpp index c3f5df739..8c8abe5f8 100644 --- a/GridKit/Solver/Dynamic/Native/StepController.hpp +++ b/GridKit/Solver/Dynamic/Native/StepController.hpp @@ -4,32 +4,35 @@ #include -namespace Integrator +namespace AnalysisManager { - /** - * @brief Interface for step size controllers. Used by `Rosenbrock` integrators to decide when to accept/reject steps and - * what size each step should be. - * - * @todo It may be best to have \ref usesError() return a reference to the \ref ErrorNorm that should be used. - */ - template - class StepController + namespace NativeDynamicSolver { - public: /** - * @brief Decide the control flow for the next step, based on information gathered by the integrator about the current step. + * @brief Interface for step size controllers. Used by `Rosenbrock` integrators to decide when to accept/reject steps and + * what size each step should be. * - * @param err The estimated error made by the current step. Only calculated if `usesError()` returns true, otherwise it is assumed this - * value is not used. - * @param prev_step The control flow from before the current step was taken. Can be used to accurately update the step size. - * @param method_order The order of the method being used. - * @return StepControl + * @todo It may be best to have \ref usesError() return a reference to the \ref ErrorNorm that should be used. */ - virtual StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) = 0; - /** - * @brief Return whether or not the `nextStep` method implementation uses the `err` parameter. If `false`, this parameter is not calculated. - * - */ - virtual bool usesError() const = 0; - }; -} // namespace Integrator + template + class StepController + { + public: + /** + * @brief Decide the control flow for the next step, based on information gathered by the integrator about the current step. + * + * @param err The estimated error made by the current step. Only calculated if `usesError()` returns true, otherwise it is assumed this + * value is not used. + * @param prev_step The control flow from before the current step was taken. Can be used to accurately update the step size. + * @param method_order The order of the method being used. + * @return StepControl + */ + virtual StepControl nextStep(RealT err, StepControl prev_step, uint8_t method_order) = 0; + /** + * @brief Return whether or not the `nextStep` method implementation uses the `err` parameter. If `false`, this parameter is not calculated. + * + */ + virtual bool usesError() const = 0; + }; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index f0d2a7aee..af3ad984b 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -23,453 +23,511 @@ return err; \ } while (false) -namespace Integrator +namespace AnalysisManager { - /** - * @brief Constructs a single-line report string out of the `StepInfo` object which is suitable to be included in a CSV file. - * - * Useful if you would like to plot step info. - * - */ - template - std::string Rosenbrock::StepInfo::csvReport() const + namespace NativeDynamicSolver { - std::stringstream out; - out << std::scientific << std::setprecision(20) - << sim_time_ << ',' - << step_size_ << ',' - << next_step_size_ << ',' - << err_est_ << ',' - << step_no_ << ',' - << skip_lu_ << ',' - << skip_f_ << ',' - << accepted_; - - return out.str(); - } - - /** - * @brief Constructs a human-readable report string out of the `StepInfo` object. - * - * Useful for debug dumps. - * - */ - template - std::string Rosenbrock::StepInfo::report() const - { - std::stringstream out; - out << std::scientific << std::setprecision(20) - << "Simulation Time: " << sim_time_ << '\n' - << "Step Size: " << step_size_ << '\n' - << "Next Step Size: " << next_step_size_ << '\n' - << "Error Estimate: " << err_est_ << '\n' - << "Step Number: " << step_no_ << '\n' - << "Skip LU: " << skip_lu_ << '\n' - << "Skip F: " << skip_f_ << '\n' - << "Accepted: " << accepted_; - - return out.str(); - } - - /** - * @brief Constructs a human-readable report string out of the `Stats` object. - * - * Useful for reporting at the end of a simulation. - * - */ - template - std::string Rosenbrock::Stats::report() const - { - std::stringstream out; - out << "Rejections: " << rejections_.size() - << "\nSteps: " << num_steps_ - << "\nSkip LU Steps: " << skip_lu_steps_.size() - << "\nMin Step: " << min_step_ - << "\nMax Step: " << max_step_ - << "\nRHS Function Evals: " << f_evals_ - << "\nRHS Function Skipped: " << f_skipped_ - << "\nJacobian Evals: " << jac_evals_ - << "\nLinear Solves: " << decomp_solves_; - - return out.str(); - } - - /** - * @brief Accumulate statistics into one object. Useful to collect statistics over runs of several simulations, such as when - * there is a discrete event. - * - * @param other The other statistics to add to this one. - * - * @todo Right now, the step numbers for \ref rejections_ and \ref skip_lu_steps_ are impossible to tell apart from the different simulations. - */ - template - typename Rosenbrock::Stats& Rosenbrock::Stats::operator+=(const Stats& other) - { - rejections_.insert(rejections_.end(), other.rejections_.begin(), other.rejections_.end()); - skip_lu_steps_.insert(skip_lu_steps_.end(), other.skip_lu_steps_.begin(), other.skip_lu_steps_.end()); - - num_steps_ += other.num_steps_; - f_evals_ += other.f_evals_; - f_skipped_ += other.f_skipped_; - jac_evals_ += other.jac_evals_; - decomp_solves_ += other.decomp_solves_; - - min_step_ = std::min(min_step_, other.min_step_); - max_step_ = std::max(max_step_, other.max_step_); - - return *this; - } - - /** - * @brief Checks to see if the \ref asum_ variable can be re-used from the previous stage. - * - * Often, a row in the Rosenbrock A matrix is the exact same as the previous row, but with an additional - * non-zero element at the end. In this case, the \ref asum_ variable is the exact same as the previous stage, - * but with one extra additional term. The integrator can take advantage of this and reduce a matmul for computing - * \ref asum_ down to a single `axpy`. - * - * Typically, \ref asum_ is only initialized if it needs to be re-calculated from scratch. For this reason, this - * function will always return `false` for `stage == 0`, forcing \ref asum_ to be initialized for the first stage. - * - * @param stage The stage being checked. - */ - template - constexpr bool Rosenbrock::Tableau::canReuseAsum(size_t stage) const - { - assert(stage < num_stages_); + /** + * @brief Constructs a single-line report string out of the `StepInfo` object which is suitable to be included in a CSV file. + * + * Useful if you would like to plot step info. + * + */ + template + std::string Rosenbrock::StepInfo::csvReport() const + { + std::stringstream out; + out << std::scientific << std::setprecision(20) + << sim_time_ << ',' + << step_size_ << ',' + << next_step_size_ << ',' + << err_est_ << ',' + << step_no_ << ',' + << skip_lu_ << ',' + << skip_f_ << ',' + << accepted_; + + return out.str(); + } + + /** + * @brief Constructs a human-readable report string out of the `StepInfo` object. + * + * Useful for debug dumps. + * + */ + template + std::string Rosenbrock::StepInfo::report() const + { + std::stringstream out; + out << std::scientific << std::setprecision(20) + << "Simulation Time: " << sim_time_ << '\n' + << "Step Size: " << step_size_ << '\n' + << "Next Step Size: " << next_step_size_ << '\n' + << "Error Estimate: " << err_est_ << '\n' + << "Step Number: " << step_no_ << '\n' + << "Skip LU: " << skip_lu_ << '\n' + << "Skip F: " << skip_f_ << '\n' + << "Accepted: " << accepted_; + + return out.str(); + } + + /** + * @brief Constructs a human-readable report string out of the `Stats` object. + * + * Useful for reporting at the end of a simulation. + * + */ + template + std::string Rosenbrock::Stats::report() const + { + std::stringstream out; + out << "Rejections: " << rejections_.size() + << "\nSteps: " << num_steps_ + << "\nSkip LU Steps: " << skip_lu_steps_.size() + << "\nMin Step: " << min_step_ + << "\nMax Step: " << max_step_ + << "\nRHS Function Evals: " << f_evals_ + << "\nRHS Function Skipped: " << f_skipped_ + << "\nJacobian Evals: " << jac_evals_ + << "\nLinear Solves: " << decomp_solves_; + + return out.str(); + } + + /** + * @brief Accumulate statistics into one object. Useful to collect statistics over runs of several simulations, such as when + * there is a discrete event. + * + * @param other The other statistics to add to this one. + * + * @todo Right now, the step numbers for \ref rejections_ and \ref skip_lu_steps_ are impossible to tell apart from the different simulations. + */ + template + typename Rosenbrock::Stats& Rosenbrock::Stats::operator+=(const Stats& other) + { + rejections_.insert(rejections_.end(), other.rejections_.begin(), other.rejections_.end()); + skip_lu_steps_.insert(skip_lu_steps_.end(), other.skip_lu_steps_.begin(), other.skip_lu_steps_.end()); + + num_steps_ += other.num_steps_; + f_evals_ += other.f_evals_; + f_skipped_ += other.f_skipped_; + jac_evals_ += other.jac_evals_; + decomp_solves_ += other.decomp_solves_; - if (stage == 0) - return false; - else + min_step_ = std::min(min_step_, other.min_step_); + max_step_ = std::max(max_step_, other.max_step_); + + return *this; + } + + /** + * @brief Checks to see if the \ref asum_ variable can be re-used from the previous stage. + * + * Often, a row in the Rosenbrock A matrix is the exact same as the previous row, but with an additional + * non-zero element at the end. In this case, the \ref asum_ variable is the exact same as the previous stage, + * but with one extra additional term. The integrator can take advantage of this and reduce a matmul for computing + * \ref asum_ down to a single `axpy`. + * + * Typically, \ref asum_ is only initialized if it needs to be re-calculated from scratch. For this reason, this + * function will always return `false` for `stage == 0`, forcing \ref asum_ to be initialized for the first stage. + * + * @param stage The stage being checked. + */ + template + constexpr bool Rosenbrock::Tableau::canReuseAsum(size_t stage) const { - for (size_t j = 0; j < stage - 1; j++) + assert(stage < num_stages_); + + if (stage == 0) + return false; + else { - if (getA(stage, j) != getA(stage - 1, j)) + for (size_t j = 0; j < stage - 1; j++) + { + if (getA(stage, j) != getA(stage - 1, j)) + { + return false; + } + } + return true; + } + } + + /** + * @brief Checks to see if the \ref asum_ variable can be re-used from the last stage to compute the output state + * + * Often, the Rosenbrock m vector is the exact same as the last row in the A matrix, but with an additional + * non-zero element at the end. In this case, the output state for a step is equal to the \ref asum_ variables from + * the last stage but with a single weighted vector added to it. The integrator can take advantage of this and reduce + * a matmul for computing the final state down to a single `axpy`. + * + * If a method has only a single stage, then \ref asum_ will not have been initialized (as it will just be equal to y0). + * Therefore, this method will return `false`. + * + */ + template + constexpr bool Rosenbrock::Tableau::canReuseAsumForOut() const + { + if (num_stages_ == 1) + return false; + + for (size_t j = 0; j < num_stages_ - 1; j++) + { + if (getA(num_stages_ - 1, j) != m_[j]) { return false; } } + return true; } - } - - /** - * @brief Checks to see if the \ref asum_ variable can be re-used from the last stage to compute the output state - * - * Often, the Rosenbrock m vector is the exact same as the last row in the A matrix, but with an additional - * non-zero element at the end. In this case, the output state for a step is equal to the \ref asum_ variables from - * the last stage but with a single weighted vector added to it. The integrator can take advantage of this and reduce - * a matmul for computing the final state down to a single `axpy`. - * - * If a method has only a single stage, then \ref asum_ will not have been initialized (as it will just be equal to y0). - * Therefore, this method will return `false`. - * - */ - template - constexpr bool Rosenbrock::Tableau::canReuseAsumForOut() const - { - if (num_stages_ == 1) - return false; - for (size_t j = 0; j < num_stages_ - 1; j++) + /** + * @brief Returns the index of a stage which can be used as an embedded error estimator, if that stage exists. + * + * Sometimes, Rosenbrock methods are designed in such a way where a stage can be used as an embedded error estimator. + * Typically, the embedded error estimator is a linear combination of the stages, so in this particular case the + * weights will be 1 on the estimator stage and 0 on all other stages. + * + * @pre This function is only valid to call if there is an embedded error estimator and its coefficients + * are included in this tableau. + */ + template + constexpr std::optional Rosenbrock::Tableau::errorEstimatorStage() const { - if (getA(num_stages_ - 1, j) != m_[j]) + assert(e_); + + std::optional re; + for (size_t j = 0; j < num_stages_; j++) { - return false; + if (e_[j] == GridKit::ONE && !re) + { + re = j; + } + else if (e_[j] != GridKit::ZERO) + { + return {}; + } } + return re; } - return true; - } - - /** - * @brief Returns the index of a stage which can be used as an embedded error estimator, if that stage exists. - * - * Sometimes, Rosenbrock methods are designed in such a way where a stage can be used as an embedded error estimator. - * Typically, the embedded error estimator is a linear combination of the stages, so in this particular case the - * weights will be 1 on the estimator stage and 0 on all other stages. - * - * @pre This function is only valid to call if there is an embedded error estimator and its coefficients - * are included in this tableau. - */ - template - constexpr std::optional Rosenbrock::Tableau::errorEstimatorStage() const - { - assert(e_); + /** + * @brief Construct a new Rosenbrock integrator. + * + * @param tab The tableau to be used for this integrator. Since tableaus contain `std::unique_ptr`, it must be moved into the integrator. + * @param model The model to be simulated. Despite taking a pointer, this must be a valid pointer to an `Evaluator`. + * Must have \ref GridKit::Model::Evaluator::tag() set, and must be in Hessenberg form (\f(F(\dot y, y) = \dot y - f(y)\f)). + * @param lin_solver The linear solver to be used when constructing stages during simulation. The reference must remain valid for as long + * as the Rosenbrock integrator lives. + * @param vector_handler The vector handler to be used when simulating. The reference must remain valid for as long as the Rosenbrock + * integrator lives. + * @param err_norm The error norm to use in calculating error for the `StepController`. Will not be accessed if the `StepController` + * does not need error (such as `FixedStep`), so `nullptr` can be passed in that circumstance. + * @param memspace The memory space that linear algebra operations should be performed in. + */ + template + Rosenbrock::Rosenbrock(Tableau&& tab, + GridKit::Model::Evaluator* model, + GridKit::LinearAlgebra::LinearSolver& lin_solver, + GridKit::LinearAlgebra::VectorHandler& vector_handler, + const ErrorNorm* err_norm, + GridKit::memory::MemorySpace memspace) + : tab_(std::move(tab)), + model_(model), + lin_solver_(lin_solver), + vector_handler_(vector_handler), + err_norm_(err_norm), + memspace_(memspace) + { + } - std::optional re; - for (size_t j = 0; j < num_stages_; j++) + /** + * @brief Allocates memory based on the the size of \ref model_. Must be called before other methods. + * + * @note This method can fail. + * + * @pre Allocates the Jacobian matrix, which requires knowledge of the number of nonzero elements. This must be known at this point, + * so `allocate()` must be called beforehand on \ref model_ to count the nonzero elements and allocate the CSR Jacobian. + * + * @return An error code, with 0 as success. + */ + template + int Rosenbrock::allocate() { - if (e_[j] == GridKit::ONE && !re) + size_t size = static_cast(model_->size()); + + y_prev_ = std::make_unique(size); + y_cur_ = std::make_unique(size); + y_new_ = std::make_unique(size); + y_interp_ = std::make_unique(size); + workspace_.asum_ = std::make_unique(size); + workspace_.csum_ = std::make_unique(size); + workspace_.RHS_ = std::make_unique(size); + workspace_.RHS_first_stage_ = std::make_unique(size); + workspace_.dFdt_ = std::make_unique(size); + workspace_.mass_ = std::make_unique(size); + + BUBBLE_FAIL(y_prev_->allocate(memspace_)); + BUBBLE_FAIL(y_cur_->allocate(memspace_)); + BUBBLE_FAIL(y_new_->allocate(memspace_)); + BUBBLE_FAIL(y_interp_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.asum_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.csum_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.RHS_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.RHS_first_stage_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.dFdt_->allocate(memspace_)); + BUBBLE_FAIL(workspace_.mass_->allocate(memspace_)); + + if (tab_.e_) { - re = j; + std::optional err_est_stage = tab_.errorEstimatorStage(); + if (!err_est_stage) + { + workspace_.err_est_ = std::make_unique(size); + BUBBLE_FAIL(workspace_.err_est_->allocate(memspace_)); + } } - else if (e_[j] != GridKit::ZERO) + + workspace_.stages_ = std::make_unique[]>(tab_.num_stages_); + for (size_t i = 0; i < tab_.num_stages_; i++) { - return {}; + workspace_.stages_[i] = std::make_unique(size); + BUBBLE_FAIL(workspace_.stages_[i]->allocate(memspace_)); + workspace_.stages_[i]->setToZero(memspace_); } - } - return re; - } - - /** - * @brief Construct a new Rosenbrock integrator. - * - * @param tab The tableau to be used for this integrator. Since tableaus contain `std::unique_ptr`, it must be moved into the integrator. - * @param model The model to be simulated. Despite taking a pointer, this must be a valid pointer to an `Evaluator`. - * Must have \ref GridKit::Model::Evaluator::tag() set, and must be in Hessenberg form (\f(F(\dot y, y) = \dot y - f(y)\f)). - * @param lin_solver The linear solver to be used when constructing stages during simulation. The reference must remain valid for as long - * as the Rosenbrock integrator lives. - * @param vector_handler The vector handler to be used when simulating. The reference must remain valid for as long as the Rosenbrock - * integrator lives. - * @param err_norm The error norm to use in calculating error for the `StepController`. Will not be accessed if the `StepController` - * does not need error (such as `FixedStep`), so `nullptr` can be passed in that circumstance. - * @param memspace The memory space that linear algebra operations should be performed in. - */ - template - Rosenbrock::Rosenbrock(Tableau&& tab, - GridKit::Model::Evaluator* model, - GridKit::LinearAlgebra::LinearSolver& lin_solver, - GridKit::LinearAlgebra::VectorHandler& vector_handler, - const ErrorNorm* err_norm, - GridKit::memory::MemorySpace memspace) - : tab_(std::move(tab)), - model_(model), - lin_solver_(lin_solver), - vector_handler_(vector_handler), - err_norm_(err_norm), - memspace_(memspace) - { - } - - /** - * @brief Allocates memory based on the the size of \ref model_. Must be called before other methods. - * - * @note This method can fail. - * - * @pre Allocates the Jacobian matrix, which requires knowledge of the number of nonzero elements. This must be known at this point, - * so `allocate()` must be called beforehand on \ref model_ to count the nonzero elements and allocate the CSR Jacobian. - * - * @return An error code, with 0 as success. - */ - template - int Rosenbrock::allocate() - { - size_t size = static_cast(model_->size()); - - y_prev_ = std::make_unique(size); - y_cur_ = std::make_unique(size); - y_new_ = std::make_unique(size); - y_interp_ = std::make_unique(size); - workspace_.asum_ = std::make_unique(size); - workspace_.csum_ = std::make_unique(size); - workspace_.RHS_ = std::make_unique(size); - workspace_.RHS_first_stage_ = std::make_unique(size); - workspace_.dFdt_ = std::make_unique(size); - workspace_.mass_ = std::make_unique(size); - - BUBBLE_FAIL(y_prev_->allocate(memspace_)); - BUBBLE_FAIL(y_cur_->allocate(memspace_)); - BUBBLE_FAIL(y_new_->allocate(memspace_)); - BUBBLE_FAIL(y_interp_->allocate(memspace_)); - BUBBLE_FAIL(workspace_.asum_->allocate(memspace_)); - BUBBLE_FAIL(workspace_.csum_->allocate(memspace_)); - BUBBLE_FAIL(workspace_.RHS_->allocate(memspace_)); - BUBBLE_FAIL(workspace_.RHS_first_stage_->allocate(memspace_)); - BUBBLE_FAIL(workspace_.dFdt_->allocate(memspace_)); - BUBBLE_FAIL(workspace_.mass_->allocate(memspace_)); - - if (tab_.e_) - { - std::optional err_est_stage = tab_.errorEstimatorStage(); - if (!err_est_stage) + + if (tab_.order_ > 2) { - workspace_.err_est_ = std::make_unique(size); - BUBBLE_FAIL(workspace_.err_est_->allocate(memspace_)); + dense_coeff_ = std::make_unique[]>(tab_.order_ - 2); + for (size_t i = 0; i < static_cast(tab_.order_ - 2); i++) + { + dense_coeff_[i] = std::make_unique(size); + BUBBLE_FAIL(dense_coeff_[i]->allocate(memspace_)); + } } - } - workspace_.stages_ = std::make_unique[]>(tab_.num_stages_); - for (size_t i = 0; i < tab_.num_stages_; i++) - { - workspace_.stages_[i] = std::make_unique(size); - BUBBLE_FAIL(workspace_.stages_[i]->allocate(memspace_)); - workspace_.stages_[i]->setToZero(memspace_); + return 0; } - if (tab_.order_ > 2) + /** + * @brief Initializes the simulation. Must be called before \ref integrate() or \ref timeStep(). Must also be called after + * discrete events. + * + * - Sets the simulation time to `t0` and copies the initial condition from \ref model_. + * - Analyzes \ref model_ Jacobian sparsity and runs the preconditioner + * - Generates the mass matrix from \ref GridKit::Model::Evaluator::tag(). If the tag is not properly set, then initialization will fail. + * - Resets \ref stats_. + * + * @note This method can fail. + * + * @pre Must have called \ref allocate(). The `model.tag_` variable must be properly constructed. + * + * @todo Document mass matrix construction + * + * @param t0 The starting simulation time. + * @return An error code, with 0 as success. + */ + template + int Rosenbrock::initializeSimulation(RealT t0) { - dense_coeff_ = std::make_unique[]>(tab_.order_ - 2); - for (size_t i = 0; i < static_cast(tab_.order_ - 2); i++) + current_time_ = t0; + BUBBLE_FAIL(y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_)); + workspace_.jacobian_analyzed_ = false; + + BUBBLE_FAIL(lin_solver_.configureSolver(*model_->getCsrJacobian())); + + if (model_->tag().size() != static_cast(model_->size())) { - dense_coeff_[i] = std::make_unique(size); - BUBBLE_FAIL(dense_coeff_[i]->allocate(memspace_)); + std::cerr << "Model tag is either unset or does not match the size of the model\n"; + return 1; } - } - return 0; - } - - /** - * @brief Initializes the simulation. Must be called before \ref integrate() or \ref timeStep(). Must also be called after - * discrete events. - * - * - Sets the simulation time to `t0` and copies the initial condition from \ref model_. - * - Analyzes \ref model_ Jacobian sparsity and runs the preconditioner - * - Generates the mass matrix from \ref GridKit::Model::Evaluator::tag(). If the tag is not properly set, then initialization will fail. - * - Resets \ref stats_. - * - * @note This method can fail. - * - * @pre Must have called \ref allocate(). The `model.tag_` variable must be properly constructed. - * - * @todo Document mass matrix construction - * - * @param t0 The starting simulation time. - * @return An error code, with 0 as success. - */ - template - int Rosenbrock::initializeSimulation(RealT t0) - { - current_time_ = t0; - BUBBLE_FAIL(y_cur_->copyFromExternal(model_->y().data(), memspace_, memspace_)); - workspace_.jacobian_analyzed_ = false; + std::unique_ptr mass = std::make_unique(model_->tag().size()); + for (size_t i = 0; i < static_cast(model_->size()); i++) + { + mass[i] = model_->tag()[i] ? GridKit::ONE : GridKit::ZERO; + } + BUBBLE_FAIL(workspace_.mass_->copyFromExternal(mass.get(), memspace_, memspace_)); - BUBBLE_FAIL(lin_solver_.configureSolver(*model_->getCsrJacobian())); + stats_ = Stats(); - if (model_->tag().size() != static_cast(model_->size())) - { - std::cerr << "Model tag is either unset or does not match the size of the model\n"; - return 1; + return 0; } - std::unique_ptr mass = std::make_unique(model_->tag().size()); - for (size_t i = 0; i < static_cast(model_->size()); i++) + /** + * @brief Simulate the given model, producing output at the given output times. + * + * Implements a simple time stepping algorithm and facilitates output. + * 1. Calls \ref timeStep() to move the simulation forward by \ref step_size_ (starting at `params.starting_step`). + * 2. Consults the `step_controller` to see if the step is accepted, and what the next \ref step_size_ should be. + * 3. If accepted, advance simulation by adding \ref step_size_ to \ref current_time_ using Kahan summation and shifting + * \ref y_new_ -> \ref y_cur_ -> \ref y_prev_. + * 4. Any time we step over an output time, calculate dense coefficients if available. + * 5. For each output time stepped over in the last step, interpolate the output at that time and call `out_cb`. + * + * Time is advanced to at least the final output time. The simulation may step over the final time, so if you + * wish to restart simulation from the final output time, make sure to re-initialize the model's state and call + * \ref initializeSimulation(). + * + * @note This method can fail. + * + * @pre Must have called \ref initializeSimulation(). + * + * @todo A higher-order Hermite interpolation can be used as a fallback for interpolation when dense coefficients don't exist. + * Each step needs to sample the residual function at the beginning (and therefore end) of the step, which contains derivative + * information. This derivative information can be re-used for Hermite interpolation. + * + * @todo It doesn't really make sense to have the error estimator separate from the step controller, since your choice of one will + * affect your choice of the other. The error estimator should probably be inside the step controller, then accessed if needed. + * + * @todo Return an error when max steps is hit. + * + * @todo Check if current time is close enough to output time, and skip interpolation + * + * @todo Configure upper bound for skip lu step size increase + * + * @param out_times The times at which output is wanted. The simulation will stop once the final output time has been reached. + * @param step_controller The step size controller to use during the simulation. + * @param params The parameters to use during the simulation. + * @param out_cb An optional function which, if provided, will be called once for each time in `out_times`. The simulation time of the + * output is passed as the only argument. Before being called, the model will be updated with the output state and can be queried + * separately by the callback if needed. + * @param step_cb An optional function which, if provided, will be called once for each step the integrator takes. Information about the + * step which was just taken is provided as the argument. Before being called, the model will be updated with the current value of the state + * an can be queried separately by the callback if needed. Useful for debugging simulations. + * @return An error code, with 0 as success. + */ + template + int Rosenbrock::integrate(const std::vector& out_times, + StepController& step_controller, + Parameters params, + std::optional> out_cb, + std::optional> step_cb) { - mass[i] = model_->tag()[i] ? GridKit::ONE : GridKit::ZERO; - } - BUBBLE_FAIL(workspace_.mass_->copyFromExternal(mass.get(), memspace_, memspace_)); - - stats_ = Stats(); - - return 0; - } - - /** - * @brief Simulate the given model, producing output at the given output times. - * - * Implements a simple time stepping algorithm and facilitates output. - * 1. Calls \ref timeStep() to move the simulation forward by \ref step_size_ (starting at `params.starting_step`). - * 2. Consults the `step_controller` to see if the step is accepted, and what the next \ref step_size_ should be. - * 3. If accepted, advance simulation by adding \ref step_size_ to \ref current_time_ using Kahan summation and shifting - * \ref y_new_ -> \ref y_cur_ -> \ref y_prev_. - * 4. Any time we step over an output time, calculate dense coefficients if available. - * 5. For each output time stepped over in the last step, interpolate the output at that time and call `out_cb`. - * - * Time is advanced to at least the final output time. The simulation may step over the final time, so if you - * wish to restart simulation from the final output time, make sure to re-initialize the model's state and call - * \ref initializeSimulation(). - * - * @note This method can fail. - * - * @pre Must have called \ref initializeSimulation(). - * - * @todo A higher-order Hermite interpolation can be used as a fallback for interpolation when dense coefficients don't exist. - * Each step needs to sample the residual function at the beginning (and therefore end) of the step, which contains derivative - * information. This derivative information can be re-used for Hermite interpolation. - * - * @todo It doesn't really make sense to have the error estimator separate from the step controller, since your choice of one will - * affect your choice of the other. The error estimator should probably be inside the step controller, then accessed if needed. - * - * @todo Return an error when max steps is hit. - * - * @todo Check if current time is close enough to output time, and skip interpolation - * - * @todo Configure upper bound for skip lu step size increase - * - * @param out_times The times at which output is wanted. The simulation will stop once the final output time has been reached. - * @param step_controller The step size controller to use during the simulation. - * @param params The parameters to use during the simulation. - * @param out_cb An optional function which, if provided, will be called once for each time in `out_times`. The simulation time of the - * output is passed as the only argument. Before being called, the model will be updated with the output state and can be queried - * separately by the callback if needed. - * @param step_cb An optional function which, if provided, will be called once for each step the integrator takes. Information about the - * step which was just taken is provided as the argument. Before being called, the model will be updated with the current value of the state - * an can be queried separately by the callback if needed. Useful for debugging simulations. - * @return An error code, with 0 as success. - */ - template - int Rosenbrock::integrate(const std::vector& out_times, - StepController& step_controller, - Parameters params, - std::optional> out_cb, - std::optional> step_cb) - { - constexpr RealT ONE = GridKit::ONE; - constexpr RealT ZERO = GridKit::ZERO; + constexpr RealT ONE = GridKit::ONE; + constexpr RealT ZERO = GridKit::ZERO; - skip_lu_ = false; - skip_f_ = false; + skip_lu_ = false; + skip_f_ = false; - bool prev_accept = true; - step_size_ = params.starting_step_; + bool prev_accept = true; + step_size_ = params.starting_step_; - double next_step_size; + double next_step_size; - // Kahan summation time buffer. The "leftover" time that was lost when trying to add h at some point that needs to be added - // later - double time_buffer = 0; + // Kahan summation time buffer. The "leftover" time that was lost when trying to add h at some point that needs to be added + // later + double time_buffer = 0; - // Generate output for each output time - for (double out_time : out_times) - { - while (current_time_ < out_time && stats_.num_steps_ < params.max_steps_) + // Generate output for each output time + for (double out_time : out_times) { - BUBBLE_FAIL(timeStep(current_time_, step_size_)); + while (current_time_ < out_time && stats_.num_steps_ < params.max_steps_) + { + BUBBLE_FAIL(timeStep(current_time_, step_size_)); - double err = 0; + double err = 0; - if (step_controller.usesError()) - { - if (err_norm_ == nullptr) + if (step_controller.usesError()) { - std::cerr << "The provided step controller requires the use of an error norm, but none was provided!\n"; + if (err_norm_ == nullptr) + { + std::cerr << "The provided step controller requires the use of an error norm, but none was provided!\n"; - return -1; - } + return -1; + } - State& err_vec = errorEstimate(); - err = err_norm_->errorNorm(err_vec, *y_new_, *y_cur_, vector_handler_, memspace_); - } + State& err_vec = errorEstimate(); + err = err_norm_->errorNorm(err_vec, *y_new_, *y_cur_, vector_handler_, memspace_); + } - StepControl next_step = step_controller.nextStep(err, - StepControl{ - .accept_ = prev_accept, - .step_size_ = step_size_, - }, - tab_.order_); - prev_accept = next_step.accept_; - next_step_size = next_step.step_size_; + StepControl next_step = step_controller.nextStep(err, + StepControl{ + .accept_ = prev_accept, + .step_size_ = step_size_, + }, + tab_.order_); + prev_accept = next_step.accept_; + next_step_size = next_step.step_size_; - if (prev_accept) - { - // Try to add the leftover time that we've stored up - double step_size_adj = step_size_ + time_buffer; - double next_time = current_time_ + step_size_adj; + if (prev_accept) + { + // Try to add the leftover time that we've stored up + double step_size_adj = step_size_ + time_buffer; + double next_time = current_time_ + step_size_adj; + + // Kahan summation - keep track of how much of step_size_adj we weren't able to add to current_time + // due to lack of precision + time_buffer = step_size_adj - (next_time - current_time_); + current_time_ = next_time; + + // Since time is advancing, we need to re-evaluate the residual function and dense coefficients + skip_f_ = false; + dense_coefficients_valid_ = false; + + stats_.num_steps_++; + if (skip_lu_) + { + stats_.skip_lu_steps_.push_back(StepInfo{ + .sim_time_ = current_time_, + .step_size_ = step_size_, + .next_step_size_ = next_step_size, + .err_est_ = err, + .step_no_ = stats_.num_steps_, + .skip_lu_ = skip_lu_, + .skip_f_ = skip_f_, + .accepted_ = prev_accept, + }); + } + stats_.min_step_ = std::min(stats_.min_step_, step_size_); + stats_.max_step_ = std::max(stats_.max_step_, step_size_); + + // Shift y_new_ -> y_cur_ -> y_prev_ + // Then y_new_ is free to be replaced (contains old y_prev_) + std::swap(y_prev_, y_cur_); + std::swap(y_cur_, y_new_); + } + else + { + skip_f_ = true; - // Kahan summation - keep track of how much of step_size_adj we weren't able to add to current_time - // due to lack of precision - time_buffer = step_size_adj - (next_time - current_time_); - current_time_ = next_time; + stats_.rejections_.push_back(StepInfo{ + .sim_time_ = current_time_, + .step_size_ = step_size_, + .next_step_size_ = next_step_size, + .err_est_ = err, + .step_no_ = stats_.num_steps_, + .skip_lu_ = skip_lu_, + .skip_f_ = skip_f_, + .accepted_ = prev_accept, + }); + } - // Since time is advancing, we need to re-evaluate the residual function and dense coefficients - skip_f_ = false; - dense_coefficients_valid_ = false; + // Check if we can use time delay Jacobians. If we would have increased the step size (but not too much), + // instead keep the step size the same and use time-delay Jacobian. + // TODO: configure upper bound here + double step_gain = next_step_size / step_size_; + if (params.skip_lu_ && step_gain >= ONE && step_gain <= 1.2) + { + skip_lu_ = true; + } + else + { + skip_lu_ = false; + prev_step_size_ = step_size_; + step_size_ = next_step_size; + } - stats_.num_steps_++; - if (skip_lu_) + // If there is a step_cb, update the model state and call it + if (step_cb) { - stats_.skip_lu_steps_.push_back(StepInfo{ + BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); + model_->updateTime(current_time_, ZERO); + + (*step_cb)(StepInfo{ .sim_time_ = current_time_, .step_size_ = step_size_, .next_step_size_ = next_step_size, @@ -480,431 +538,376 @@ namespace Integrator .accepted_ = prev_accept, }); } - stats_.min_step_ = std::min(stats_.min_step_, step_size_); - stats_.max_step_ = std::max(stats_.max_step_, step_size_); - - // Shift y_new_ -> y_cur_ -> y_prev_ - // Then y_new_ is free to be replaced (contains old y_prev_) - std::swap(y_prev_, y_cur_); - std::swap(y_cur_, y_new_); - } - else - { - skip_f_ = true; - - stats_.rejections_.push_back(StepInfo{ - .sim_time_ = current_time_, - .step_size_ = step_size_, - .next_step_size_ = next_step_size, - .err_est_ = err, - .step_no_ = stats_.num_steps_, - .skip_lu_ = skip_lu_, - .skip_f_ = skip_f_, - .accepted_ = prev_accept, - }); } - // Check if we can use time delay Jacobians. If we would have increased the step size (but not too much), - // instead keep the step size the same and use time-delay Jacobian. - // TODO: configure upper bound here - double step_gain = next_step_size / step_size_; - if (params.skip_lu_ && step_gain >= ONE && step_gain <= 1.2) - { - skip_lu_ = true; - } - else + // Check to make sure integration was paused because we reached an output time. + // Other reasons, like hitting max step count, shouldn't generate output. + if (current_time_ >= out_time) { - skip_lu_ = false; - prev_step_size_ = step_size_; - step_size_ = next_step_size; - } + // Theta = (t - t0) / h = (t - t1) / h + 1 + // current_time_ is t1 here + RealT theta = (out_time - current_time_) / prev_step_size_ + ONE; - // If there is a step_cb, update the model state and call it - if (step_cb) - { - BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(current_time_, ZERO); - - (*step_cb)(StepInfo{ - .sim_time_ = current_time_, - .step_size_ = step_size_, - .next_step_size_ = next_step_size, - .err_est_ = err, - .step_no_ = stats_.num_steps_, - .skip_lu_ = skip_lu_, - .skip_f_ = skip_f_, - .accepted_ = prev_accept, - }); - } - } - - // Check to make sure integration was paused because we reached an output time. - // Other reasons, like hitting max step count, shouldn't generate output. - if (current_time_ >= out_time) - { - // Theta = (t - t0) / h = (t - t1) / h + 1 - // current_time_ is t1 here - RealT theta = (out_time - current_time_) / prev_step_size_ + ONE; + // Generate output at the appropriate time. + if (tab_.hasDenseOutput()) + { + if (!dense_coefficients_valid_) + { + BUBBLE_FAIL(calcDenseCoeff()); + dense_coefficients_valid_ = true; + } - // Generate output at the appropriate time. - if (tab_.hasDenseOutput()) - { - if (!dense_coefficients_valid_) + BUBBLE_FAIL(interpDense(theta)); + } + else { - BUBBLE_FAIL(calcDenseCoeff()); - dense_coefficients_valid_ = true; + // TODO: Put code for alternative interpolation (Abdou) here + BUBBLE_FAIL(y_interp_->copyFromExternal(*y_prev_, memspace_, memspace_)); + vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); + vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); } - BUBBLE_FAIL(interpDense(theta)); + // Update model with output and call out_cb if it exists + if (out_cb) + { + BUBBLE_FAIL(y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_)); + model_->updateTime(out_time, ZERO); + + (*out_cb)(out_time); + } } else { - // TODO: Put code for alternative interpolation (Abdou) here - BUBBLE_FAIL(y_interp_->copyFromExternal(*y_prev_, memspace_, memspace_)); - vector_handler_.scal(1 - theta, y_interp_.get(), memspace_); - vector_handler_.axpy(theta, y_cur_.get(), y_interp_.get(), memspace_); - } - - // Update model with output and call out_cb if it exists - if (out_cb) - { - BUBBLE_FAIL(y_interp_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(out_time, ZERO); - - (*out_cb)(out_time); + BUBBLE_FAIL(y_interp_->copyFromExternal(*y_cur_, memspace_, memspace_)); + break; } } - else - { - BUBBLE_FAIL(y_interp_->copyFromExternal(*y_cur_, memspace_, memspace_)); - break; - } - } - - return 0; - } - - /** - * @brief Advance the simulation forward by one step, storing the new state in \ref y_new_. - * - * Apply the Rosenbrock scheme using the stored tableau. Each stage \f(u_i\f) is calculated as - * - * \f[\left(J - \frac{1}{h\gamma} M\right)u_i = -f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j,\f] - * - * and the next state \f(y_1\f) is calculated as - * - * \f[y_1 = y_0 + \sum_{j = 1}^sm_ju_j\f] - * - * where the coefficients \f(\gamma, \alpha_i, a_{ij}, c_{ij}, m_j\f) come from the tableau, and the mass matrix \f(M\f) and Jacobian \f(J\f) come from the model. - * - * This method uses some state which is maintained between calls for future calls to `timeStep()` and to communicate with \ref integrate(): - * - \ref y_cur_ is used as \f(y_0\f) and \ref y_new_ is used as \f(y_1\f) - * - \ref asum_ is used as \f(y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\f) for every stage except the first. Often, \f(a_{ij} = a_{i-1,j}\f), so this variable - * can be re-used between stages to save computation. Similarly, often \f(a_{ij} = m_j\f), so this variable can be re-used for computing \ref y_new_. - * - \ref csum_ is used as \f(M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f) for every stage except the first - * - \ref RHS_ is used as \f(-f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f), i.e. - * the residual evaluated at \ref asum_ plus \ref csum_, for every stage but the first. This is used as the right-hand side vector for the linear solve to solve - * for the stage value \f(u_i\f). - * - \ref RHS_first_stage_ is used as a special \ref RHS_ for the first stage, since it may need to be saved for the next step for the \ref skip_f_ flag. - * Since \f(\alpha_1 = 0\f) always, this will have a value of \f(f \left(t_0, y_0\right)\f). - * - \ref stages_ stores all of the stages \f(u_i\f). These stages are necessary to be used in future calls to \ref errorEstimate() and \ref calcDenseCoeff(). - * - \ref skip_lu_ is used as a flag set by \ref integrate() to indicate that it is appropriate to use a time-delay Jacobian by re-using the factorization - * of the last step. - * - \ref skip_f_ is used as a flag set by \ref integrate() to indicate that \f(t_0, y_0\f) have not changed since the last time `timeStep()` was called, - * so \ref RHS_first_stage_ can be re-used from the previous step. This will only happen when a step was rejected, so \ref skip_lu_ should always be false, - * and the entire first stage can't be re-used. - * - \ref jacobian_analyzed_ keeps track of whether the Jacobian has been factored in a previous call to `timeStep()`. If so, then it can be re-factored - * in a faster way. The first factor must be done on actual data, so it cannot be performed pre-simulation. - * - * @pre Must call \ref initializeSimulation() beforehand. - * - * @note This method can fail. - * - * @todo This currently does not work with non-autonomous models. Some thought needs to be put in for how we want non-autonomous models to work. - * - * @todo It doesn't really make sense to pass `t0` here, which may be inconsistent with \ref current_time_. - * This function should just use \ref current_time_ in place of `t0`. - * - * @todo It doesn't really make sense for this method to be `public` since it relies on proper setup via \ref integrate(). - * - * @todo May be able to move the copying of model jacobian pointers to \ref allocate(). - * - * @todo Since \ref csum_ is multiplied by the mass matrix (which is currently diagonal with 0s for algebraic components), we can save some computation - * by only computing the differential parts of \ref csum_ and storing it in the same variable as \ref RHS_. - * - * @param t0 \f(t_0\f) in the above formula - * @param dt \f(h\f) in the above formula. The next state \f(y_1\f) will be an estimate of the state at \f(t_1 = t_0 + h\f). - * @return An error code, with 0 as success. - */ - template - int Rosenbrock::timeStep(RealT t0, RealT dt) - { - constexpr RealT ZERO = GridKit::ZERO; - constexpr RealT MINUS_ONE = GridKit::MINUS_ONE; - // A flag to keep track of if y0 (stored in y_cur_) has been copied in to the model already, to avoid double-copying - // for evaluating the Jacobian and residual on stage 1 (both evaluated at y0). - bool y0_copied = false; + return 0; + } - // Form the left-hand side of the system. This is constant between stages. - // Can sometimes be skipped if the method allows for time-delay Jacobians (such as w-methods). - [[likely]] - if (!tab_.is_w_ || !skip_lu_) + /** + * @brief Advance the simulation forward by one step, storing the new state in \ref y_new_. + * + * Apply the Rosenbrock scheme using the stored tableau. Each stage \f(u_i\f) is calculated as + * + * \f[\left(J - \frac{1}{h\gamma} M\right)u_i = -f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j,\f] + * + * and the next state \f(y_1\f) is calculated as + * + * \f[y_1 = y_0 + \sum_{j = 1}^sm_ju_j\f] + * + * where the coefficients \f(\gamma, \alpha_i, a_{ij}, c_{ij}, m_j\f) come from the tableau, and the mass matrix \f(M\f) and Jacobian \f(J\f) come from the model. + * + * This method uses some state which is maintained between calls for future calls to `timeStep()` and to communicate with \ref integrate(): + * - \ref y_cur_ is used as \f(y_0\f) and \ref y_new_ is used as \f(y_1\f) + * - \ref asum_ is used as \f(y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\f) for every stage except the first. Often, \f(a_{ij} = a_{i-1,j}\f), so this variable + * can be re-used between stages to save computation. Similarly, often \f(a_{ij} = m_j\f), so this variable can be re-used for computing \ref y_new_. + * - \ref csum_ is used as \f(M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f) for every stage except the first + * - \ref RHS_ is used as \f(-f \left(t_0 + \alpha_ih, y_0 + \sum_{j = 1}^{i - 1} a_{ij}u_j\right) - M \sum_{j=1}^{i-1} \left(\frac{c_{ij}}{h}\right)u_j\f), i.e. + * the residual evaluated at \ref asum_ plus \ref csum_, for every stage but the first. This is used as the right-hand side vector for the linear solve to solve + * for the stage value \f(u_i\f). + * - \ref RHS_first_stage_ is used as a special \ref RHS_ for the first stage, since it may need to be saved for the next step for the \ref skip_f_ flag. + * Since \f(\alpha_1 = 0\f) always, this will have a value of \f(f \left(t_0, y_0\right)\f). + * - \ref stages_ stores all of the stages \f(u_i\f). These stages are necessary to be used in future calls to \ref errorEstimate() and \ref calcDenseCoeff(). + * - \ref skip_lu_ is used as a flag set by \ref integrate() to indicate that it is appropriate to use a time-delay Jacobian by re-using the factorization + * of the last step. + * - \ref skip_f_ is used as a flag set by \ref integrate() to indicate that \f(t_0, y_0\f) have not changed since the last time `timeStep()` was called, + * so \ref RHS_first_stage_ can be re-used from the previous step. This will only happen when a step was rejected, so \ref skip_lu_ should always be false, + * and the entire first stage can't be re-used. + * - \ref jacobian_analyzed_ keeps track of whether the Jacobian has been factored in a previous call to `timeStep()`. If so, then it can be re-factored + * in a faster way. The first factor must be done on actual data, so it cannot be performed pre-simulation. + * + * @pre Must call \ref initializeSimulation() beforehand. + * + * @note This method can fail. + * + * @todo This currently does not work with non-autonomous models. Some thought needs to be put in for how we want non-autonomous models to work. + * + * @todo It doesn't really make sense to pass `t0` here, which may be inconsistent with \ref current_time_. + * This function should just use \ref current_time_ in place of `t0`. + * + * @todo It doesn't really make sense for this method to be `public` since it relies on proper setup via \ref integrate(). + * + * @todo May be able to move the copying of model jacobian pointers to \ref allocate(). + * + * @todo Since \ref csum_ is multiplied by the mass matrix (which is currently diagonal with 0s for algebraic components), we can save some computation + * by only computing the differential parts of \ref csum_ and storing it in the same variable as \ref RHS_. + * + * @param t0 \f(t_0\f) in the above formula + * @param dt \f(h\f) in the above formula. The next state \f(y_1\f) will be an estimate of the state at \f(t_1 = t_0 + h\f). + * @return An error code, with 0 as success. + */ + template + int Rosenbrock::timeStep(RealT t0, RealT dt) { - BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); - y0_copied = true; - - // GridKit, like IDA, expects to evaluate the Jacobian J = df/dy + alpha * df/dy', - // so we need a negative here since df/dy' = M. - model_->updateTime(t0, MINUS_ONE / (dt * tab_.gamma_)); - BUBBLE_FAIL(model_->evaluateJacobian()); - BUBBLE_FAIL(lin_solver_.setupSolver(workspace_.jacobian_analyzed_)); - workspace_.jacobian_analyzed_ = true; + constexpr RealT ZERO = GridKit::ZERO; + constexpr RealT MINUS_ONE = GridKit::MINUS_ONE; - stats_.jac_evals_++; - } + // A flag to keep track of if y0 (stored in y_cur_) has been copied in to the model already, to avoid double-copying + // for evaluating the Jacobian and residual on stage 1 (both evaluated at y0). + bool y0_copied = false; - // First stage - [[unlikely]] - if (skip_f_) - { - stats_.f_skipped_++; - } - else - { - // TODO: non-autonomous model - if (!y0_copied) + // Form the left-hand side of the system. This is constant between stages. + // Can sometimes be skipped if the method allows for time-delay Jacobians (such as w-methods). + [[likely]] + if (!tab_.is_w_ || !skip_lu_) { BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); y0_copied = true; - } - model_->updateTime(t0, ZERO); - BUBBLE_FAIL(model_->evaluateResidual()); - BUBBLE_FAIL(workspace_.RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); - vector_handler_.scal(-1, workspace_.RHS_first_stage_.get(), memspace_); - stats_.f_evals_++; - } - BUBBLE_FAIL(lin_solver_.solve(*workspace_.RHS_first_stage_, *workspace_.stages_[0])); - stats_.decomp_solves_++; + // GridKit, like IDA, expects to evaluate the Jacobian J = df/dy + alpha * df/dy', + // so we need a negative here since df/dy' = M. + model_->updateTime(t0, MINUS_ONE / (dt * tab_.gamma_)); + BUBBLE_FAIL(model_->evaluateJacobian()); + BUBBLE_FAIL(lin_solver_.setupSolver(workspace_.jacobian_analyzed_)); + workspace_.jacobian_analyzed_ = true; - // Rest of stages - for (size_t i = 1; i < tab_.num_stages_; i++) - { - // Calculate asum - // We can sometimes reuse asum from the previous stage - if (i > 1 && tab_.canReuseAsum(i)) + stats_.jac_evals_++; + } + + // First stage + [[unlikely]] + if (skip_f_) { - if (tab_.A_[tab_.num_stages_ * i + i - 1] != ZERO) - vector_handler_.axpy(tab_.A_[tab_.num_stages_ * i + i - 1], workspace_.stages_[i - 1].get(), workspace_.asum_.get(), memspace_); + stats_.f_skipped_++; } else { - BUBBLE_FAIL(workspace_.asum_->copyFromExternal(*y_cur_, memspace_, memspace_)); - - for (size_t j = 0; j < i; j++) + // TODO: non-autonomous model + if (!y0_copied) { - if (tab_.A_[tab_.num_stages_ * i + j] != ZERO) - vector_handler_.axpy(tab_.A_[tab_.num_stages_ * i + j], workspace_.stages_[j].get(), workspace_.asum_.get(), memspace_); + BUBBLE_FAIL(y_cur_->copyToExternal(model_->y().data(), memspace_, memspace_)); + y0_copied = true; } + model_->updateTime(t0, ZERO); + BUBBLE_FAIL(model_->evaluateResidual()); + BUBBLE_FAIL(workspace_.RHS_first_stage_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_)); + vector_handler_.scal(-1, workspace_.RHS_first_stage_.get(), memspace_); + + stats_.f_evals_++; } + BUBBLE_FAIL(lin_solver_.solve(*workspace_.RHS_first_stage_, *workspace_.stages_[0])); + stats_.decomp_solves_++; - // Calculate csum - // TODO: Since csum is multiplied by the mass matrix, we can reduce calculations by just not calculating some indices - BUBBLE_FAIL(workspace_.csum_->setToZero(memspace_)); - for (size_t j = 0; j < i; j++) + // Rest of stages + for (size_t i = 1; i < tab_.num_stages_; i++) { - if (tab_.C_[i * tab_.num_stages_ + j] != ZERO) + // Calculate asum + // We can sometimes reuse asum from the previous stage + if (i > 1 && tab_.canReuseAsum(i)) { - vector_handler_.axpy(tab_.C_[i * tab_.num_stages_ + j] / dt, workspace_.stages_[j].get(), workspace_.csum_.get(), memspace_); + if (tab_.A_[tab_.num_stages_ * i + i - 1] != ZERO) + vector_handler_.axpy(tab_.A_[tab_.num_stages_ * i + i - 1], workspace_.stages_[i - 1].get(), workspace_.asum_.get(), memspace_); } - } + else + { + BUBBLE_FAIL(workspace_.asum_->copyFromExternal(*y_cur_, memspace_, memspace_)); - // TODO: non-autonomous model - BUBBLE_FAIL(workspace_.asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); - model_->updateTime(t0 + tab_.alpha_sum_[i] * dt, ZERO); - BUBBLE_FAIL(model_->evaluateResidual()); - workspace_.RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); + for (size_t j = 0; j < i; j++) + { + if (tab_.A_[tab_.num_stages_ * i + j] != ZERO) + vector_handler_.axpy(tab_.A_[tab_.num_stages_ * i + j], workspace_.stages_[j].get(), workspace_.asum_.get(), memspace_); + } + } - vector_handler_.scal(MINUS_ONE, workspace_.RHS_.get(), memspace_); - vector_handler_.scal(workspace_.mass_.get(), workspace_.csum_.get(), memspace_); - vector_handler_.axpy(MINUS_ONE, workspace_.csum_.get(), workspace_.RHS_.get(), memspace_); + // Calculate csum + // TODO: Since csum is multiplied by the mass matrix, we can reduce calculations by just not calculating some indices + BUBBLE_FAIL(workspace_.csum_->setToZero(memspace_)); + for (size_t j = 0; j < i; j++) + { + if (tab_.C_[i * tab_.num_stages_ + j] != ZERO) + { + vector_handler_.axpy(tab_.C_[i * tab_.num_stages_ + j] / dt, workspace_.stages_[j].get(), workspace_.csum_.get(), memspace_); + } + } - BUBBLE_FAIL(lin_solver_.solve(*workspace_.RHS_, *workspace_.stages_[i])); - stats_.f_evals_++; - stats_.decomp_solves_++; - } + // TODO: non-autonomous model + BUBBLE_FAIL(workspace_.asum_->copyToExternal(model_->y().data(), memspace_, memspace_)); + model_->updateTime(t0 + tab_.alpha_sum_[i] * dt, ZERO); + BUBBLE_FAIL(model_->evaluateResidual()); + workspace_.RHS_->copyFromExternal(model_->getResidual().data(), memspace_, memspace_); - // Compute the solution at time t + dt - // It happens often where the solution is just asum of the last stage - // plus some multiple of the last stage. In that case we can avoid a matmul - if (tab_.canReuseAsumForOut()) - { - std::swap(workspace_.asum_, y_new_); - vector_handler_.axpy(tab_.m_[tab_.num_stages_ - 1], workspace_.stages_[tab_.num_stages_ - 1].get(), y_new_.get(), memspace_); - } - else - { - BUBBLE_FAIL(y_new_->copyFromExternal(*y_cur_, memspace_, memspace_)); + vector_handler_.scal(MINUS_ONE, workspace_.RHS_.get(), memspace_); + vector_handler_.scal(workspace_.mass_.get(), workspace_.csum_.get(), memspace_); + vector_handler_.axpy(MINUS_ONE, workspace_.csum_.get(), workspace_.RHS_.get(), memspace_); + + BUBBLE_FAIL(lin_solver_.solve(*workspace_.RHS_, *workspace_.stages_[i])); + stats_.f_evals_++; + stats_.decomp_solves_++; + } - for (size_t j = 0; j < tab_.num_stages_; j++) + // Compute the solution at time t + dt + // It happens often where the solution is just asum of the last stage + // plus some multiple of the last stage. In that case we can avoid a matmul + if (tab_.canReuseAsumForOut()) { - if (tab_.m_[j] != ZERO) + std::swap(workspace_.asum_, y_new_); + vector_handler_.axpy(tab_.m_[tab_.num_stages_ - 1], workspace_.stages_[tab_.num_stages_ - 1].get(), y_new_.get(), memspace_); + } + else + { + BUBBLE_FAIL(y_new_->copyFromExternal(*y_cur_, memspace_, memspace_)); + + for (size_t j = 0; j < tab_.num_stages_; j++) { - vector_handler_.axpy(tab_.m_[j], workspace_.stages_[j].get(), y_new_.get(), memspace_); + if (tab_.m_[j] != ZERO) + { + vector_handler_.axpy(tab_.m_[j], workspace_.stages_[j].get(), y_new_.get(), memspace_); + } } } - } - return 0; - } - - /** - * @brief Calculates an estimation of the error produced by the last call to \ref timeStep() which can be used as the - * `err` argument for \ref ErrorNorm::errorNorm(). - * - * Calculate the embedded error as - * - * \f[\hat{e} = \sum_{j = 1}^s e_j u_j,\f] - * - * where \f(e_j\f) are tableau coefficients and \f(u_j\f) are stages computed by \ref timeStep(). It happens often that - * \f(e_j = 0\f) for all but one stage, where \f(e_j = 1\f). In that case, the stage itself is used as the error estimate, - * and extra calculation can be avoided. For this reason, a reference to the estimate is returned to avoid an unnecessary copy. - * - * @pre Must call \ref timeStep(), and tableau must have coefficients for an embedded error estimator. - * @note This method can fail. - * - * @todo This function is fallible, but the return type makes it difficult to return an error code. Right now it will throw an - * error, but it should be refactored to allow returning an error code, such as by returning `std::variant`. - * - * @return A reference to the estimated error. - */ - template - Rosenbrock::State& Rosenbrock::errorEstimate() const - { - // Test to see if the tableau allows us to use a stage as the error estimate, - // avoiding extra computation. - std::optional err_stage = tab_.errorEstimatorStage(); - - if (err_stage) - { - return *workspace_.stages_[*err_stage]; + return 0; } - else + + /** + * @brief Calculates an estimation of the error produced by the last call to \ref timeStep() which can be used as the + * `err` argument for \ref ErrorNorm::errorNorm(). + * + * Calculate the embedded error as + * + * \f[\hat{e} = \sum_{j = 1}^s e_j u_j,\f] + * + * where \f(e_j\f) are tableau coefficients and \f(u_j\f) are stages computed by \ref timeStep(). It happens often that + * \f(e_j = 0\f) for all but one stage, where \f(e_j = 1\f). In that case, the stage itself is used as the error estimate, + * and extra calculation can be avoided. For this reason, a reference to the estimate is returned to avoid an unnecessary copy. + * + * @pre Must call \ref timeStep(), and tableau must have coefficients for an embedded error estimator. + * @note This method can fail. + * + * @todo This function is fallible, but the return type makes it difficult to return an error code. Right now it will throw an + * error, but it should be refactored to allow returning an error code, such as by returning `std::variant`. + * + * @return A reference to the estimated error. + */ + template + Rosenbrock::State& Rosenbrock::errorEstimate() const { - // TODO: could make this function return recoverable errors by using std::variant - int err_code = workspace_.err_est_->copyFromExternal(*workspace_.stages_[0], memspace_, memspace_); + // Test to see if the tableau allows us to use a stage as the error estimate, + // avoiding extra computation. + std::optional err_stage = tab_.errorEstimatorStage(); - if (err_code) + if (err_stage) { - throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); + return *workspace_.stages_[*err_stage]; } - - vector_handler_.scal(tab_.e_[0], workspace_.err_est_.get(), memspace_); - for (size_t j = 1; j < tab_.num_stages_; j++) + else { - if (tab_.e_[j] != GridKit::ZERO) + // TODO: could make this function return recoverable errors by using std::variant + int err_code = workspace_.err_est_->copyFromExternal(*workspace_.stages_[0], memspace_, memspace_); + + if (err_code) { - vector_handler_.axpy(tab_.e_[j], workspace_.stages_[j].get(), workspace_.err_est_.get(), memspace_); + throw std::format("ReSolve::vector::Vector::copyFromExternal failed with error code {}", err_code); } - } - return *workspace_.err_est_; - } - } - - /** - * @brief Calculate the interpolation nodes used by \ref interpDense() based on \ref stages_ computed by - * the last call to \ref timeStep(). Nodes are stored in \ref dense_coeff_. Only needs to be called once, - * but can be invalidated by future calls to \ref timeStep(). \ref integrate() keeps track of \ref dense_coefficients_valid_, - * which tells you if this function is needed to be called. - * - * @pre Must call \ref timeStep() first. - * - * @note This method can fail. - * - * @return An error code, with 0 as success. - */ - template - int Rosenbrock::calcDenseCoeff() - { - if (tab_.order_ > 2) - { - for (size_t j = 0; j < static_cast(tab_.order_ - 2); j++) - { - BUBBLE_FAIL(dense_coeff_[j]->setToZero(memspace_)); + vector_handler_.scal(tab_.e_[0], workspace_.err_est_.get(), memspace_); + for (size_t j = 1; j < tab_.num_stages_; j++) + { + if (tab_.e_[j] != GridKit::ZERO) + { + vector_handler_.axpy(tab_.e_[j], workspace_.stages_[j].get(), workspace_.err_est_.get(), memspace_); + } + } + + return *workspace_.err_est_; } + } - for (size_t i = 0; i < tab_.num_stages_; i++) + /** + * @brief Calculate the interpolation nodes used by \ref interpDense() based on \ref stages_ computed by + * the last call to \ref timeStep(). Nodes are stored in \ref dense_coeff_. Only needs to be called once, + * but can be invalidated by future calls to \ref timeStep(). \ref integrate() keeps track of \ref dense_coefficients_valid_, + * which tells you if this function is needed to be called. + * + * @pre Must call \ref timeStep() first. + * + * @note This method can fail. + * + * @return An error code, with 0 as success. + */ + template + int Rosenbrock::calcDenseCoeff() + { + if (tab_.order_ > 2) { for (size_t j = 0; j < static_cast(tab_.order_ - 2); j++) { - vector_handler_.axpy(tab_.H_[j * tab_.num_stages_ + i], workspace_.stages_[i].get(), dense_coeff_[j].get(), memspace_); + BUBBLE_FAIL(dense_coeff_[j]->setToZero(memspace_)); + } + + for (size_t i = 0; i < tab_.num_stages_; i++) + { + for (size_t j = 0; j < static_cast(tab_.order_ - 2); j++) + { + vector_handler_.axpy(tab_.H_[j * tab_.num_stages_ + i], workspace_.stages_[i].get(), dense_coeff_[j].get(), memspace_); + } } } - } - return 0; - } - - /** - * @brief Calculate an interpolated state at \f(\theta = \frac{t - t_0}{h}\f) between the initial state - * \f(y_0\f) and final state \f(y_1\f) of the last step taken using dense interpolation nodes calculated - * by \ref calcDenseCoeff(). - * - * For a valid interpolation of appropriate order, \f(\theta\f) must be in \f([0, 1]\f), although values - * beyond 1 can be used for extrapolation if desired. - * - * Uses a number of interpolation nodes equal to the order. Since \f(y_0\f) and \f(y_1\f) are interpolation - * nodes, this method will only access \ref dense_coeff_ if the method's order is greater than 2. - * - * The inteporlation is calculated as - * - * \f[y(\theta) = (1 - \theta) y_0 + \theta \left(y_1 + (1 - \theta) \sum_{i = 1}^{p-2} \theta^{i-1} \hat{y}_i\right),\f] - * - * where \f(\hat{y}_i\f) are the dense interpolation nodes calculated in \ref calcDenseCoeff() and \f(p\f) is the order of the method. - * This calculation is carried out using synthetic division. - * - * @pre Must call \ref calcDenseCoeff() first. - * - * @note This method can fail. - * - * @note If `theta` is very close to 0 or very close to 1, it might be better to simply use \ref y_prev_ or \ref y_cur_ instead of - * calculating this interpolation. - * - * @param theta The fraction of time during the last step taken to calculate the interpolation at. \f(\theta = \frac{t - t_0}{h}\f) - * @return An error code, with 0 as success. - */ - template - int Rosenbrock::interpDense(RealT theta) - { - constexpr RealT ONE = GridKit::ONE; + return 0; + } - if (tab_.order_ > 2) + /** + * @brief Calculate an interpolated state at \f(\theta = \frac{t - t_0}{h}\f) between the initial state + * \f(y_0\f) and final state \f(y_1\f) of the last step taken using dense interpolation nodes calculated + * by \ref calcDenseCoeff(). + * + * For a valid interpolation of appropriate order, \f(\theta\f) must be in \f([0, 1]\f), although values + * beyond 1 can be used for extrapolation if desired. + * + * Uses a number of interpolation nodes equal to the order. Since \f(y_0\f) and \f(y_1\f) are interpolation + * nodes, this method will only access \ref dense_coeff_ if the method's order is greater than 2. + * + * The inteporlation is calculated as + * + * \f[y(\theta) = (1 - \theta) y_0 + \theta \left(y_1 + (1 - \theta) \sum_{i = 1}^{p-2} \theta^{i-1} \hat{y}_i\right),\f] + * + * where \f(\hat{y}_i\f) are the dense interpolation nodes calculated in \ref calcDenseCoeff() and \f(p\f) is the order of the method. + * This calculation is carried out using synthetic division. + * + * @pre Must call \ref calcDenseCoeff() first. + * + * @note This method can fail. + * + * @note If `theta` is very close to 0 or very close to 1, it might be better to simply use \ref y_prev_ or \ref y_cur_ instead of + * calculating this interpolation. + * + * @param theta The fraction of time during the last step taken to calculate the interpolation at. \f(\theta = \frac{t - t_0}{h}\f) + * @return An error code, with 0 as success. + */ + template + int Rosenbrock::interpDense(RealT theta) { - BUBBLE_FAIL(y_interp_->copyFromExternal(*dense_coeff_[tab_.order_ - 3], memspace_, memspace_)); + constexpr RealT ONE = GridKit::ONE; - for (size_t i = 1; i < static_cast(tab_.order_ - 2); i++) + if (tab_.order_ > 2) { - vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(ONE, dense_coeff_[tab_.order_ - 3 - i].get(), y_interp_.get(), memspace_); - } + BUBBLE_FAIL(y_interp_->copyFromExternal(*dense_coeff_[tab_.order_ - 3], memspace_, memspace_)); - // TODO: This scal can be removed and absorbed into the next axpy, except that it currently isn't possible to put a scalar - // multiple on the y term. - vector_handler_.scal(ONE - theta, y_interp_.get(), memspace_); - } - else - { - BUBBLE_FAIL(y_interp_->setToZero(memspace_)); - } + for (size_t i = 1; i < static_cast(tab_.order_ - 2); i++) + { + vector_handler_.scal(theta, y_interp_.get(), memspace_); + vector_handler_.axpy(ONE, dense_coeff_[tab_.order_ - 3 - i].get(), y_interp_.get(), memspace_); + } - vector_handler_.axpy(ONE, y_cur_.get(), y_interp_.get(), memspace_); - vector_handler_.scal(theta, y_interp_.get(), memspace_); - vector_handler_.axpy(ONE - theta, y_prev_.get(), y_interp_.get(), memspace_); + // TODO: This scal can be removed and absorbed into the next axpy, except that it currently isn't possible to put a scalar + // multiple on the y term. + vector_handler_.scal(ONE - theta, y_interp_.get(), memspace_); + } + else + { + BUBBLE_FAIL(y_interp_->setToZero(memspace_)); + } + + vector_handler_.axpy(ONE, y_cur_.get(), y_interp_.get(), memspace_); + vector_handler_.scal(theta, y_interp_.get(), memspace_); + vector_handler_.axpy(ONE - theta, y_prev_.get(), y_interp_.get(), memspace_); - return 0; - } + return 0; + } - template class Rosenbrock; -} // namespace Integrator + template class Rosenbrock; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/Rosenbrock.hpp b/GridKit/Solver/Dynamic/Rosenbrock.hpp index d9a91986a..234d94fb3 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.hpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.hpp @@ -17,498 +17,501 @@ #include #include -namespace Integrator +namespace AnalysisManager { - /** - * @brief A Rosenbrock integrator designed to simulate a `GridKit::Model::Evaluator` model. - * Rosenbrock integrators are best for quick medium-accuracy simulation and - * boast a large amount of customization to simulating a given model. - * - * For the list of available Rosenbrock methods, see `Rosenbrock::Tableau`. - */ - template - class Rosenbrock + namespace NativeDynamicSolver { - using RealT = typename GridKit::ScalarTraits::RealT; - using State = GridKit::LinearAlgebra::Vector; - - public: /** - * @brief Keeps track of a variety of notable properties of a single step. + * @brief A Rosenbrock integrator designed to simulate a `GridKit::Model::Evaluator` model. + * Rosenbrock integrators are best for quick medium-accuracy simulation and + * boast a large amount of customization to simulating a given model. * + * For the list of available Rosenbrock methods, see `Rosenbrock::Tableau`. */ - struct StepInfo + template + class Rosenbrock { + using RealT = typename GridKit::ScalarTraits::RealT; + using State = GridKit::LinearAlgebra::Vector; + + public: /** - * @brief The simulation time at the beginning of the step - * - */ - RealT sim_time_; - /** - * @brief The size of the step. - * - */ - RealT step_size_; - /** - * @brief The size of the next step, as governed by the current `StepController` in use. - * - */ - RealT next_step_size_; - /** - * @brief The estimated error made by the step, as calculated by the current `ErrorNorm` in use. - * - */ - RealT err_est_; - /** - * @brief The step number, starting at 1. - * - */ - size_t step_no_; - /** - * @brief Whether or not the integrator decided to skip computing the decomposition of the Jacobian on this step. - * - */ - bool skip_lu_; - /** - * @brief Whether or not the integrator decided to skip evaluating the residual on the first stage on this step. - * - */ - bool skip_f_; - /** - * @brief Whether or not this step was accepted by the `StepController` in use. + * @brief Keeps track of a variety of notable properties of a single step. * */ - bool accepted_; + struct StepInfo + { + /** + * @brief The simulation time at the beginning of the step + * + */ + RealT sim_time_; + /** + * @brief The size of the step. + * + */ + RealT step_size_; + /** + * @brief The size of the next step, as governed by the current `StepController` in use. + * + */ + RealT next_step_size_; + /** + * @brief The estimated error made by the step, as calculated by the current `ErrorNorm` in use. + * + */ + RealT err_est_; + /** + * @brief The step number, starting at 1. + * + */ + size_t step_no_; + /** + * @brief Whether or not the integrator decided to skip computing the decomposition of the Jacobian on this step. + * + */ + bool skip_lu_; + /** + * @brief Whether or not the integrator decided to skip evaluating the residual on the first stage on this step. + * + */ + bool skip_f_; + /** + * @brief Whether or not this step was accepted by the `StepController` in use. + * + */ + bool accepted_; + + std::string csvReport() const; + std::string report() const; + }; + + /** + * @brief Keeps track of running statistics of the integrator since the last `initializeSimulation()` call. + * + */ + struct Stats + { + /** + * @brief Information of each step which has been rejected. + * + */ + std::vector rejections_; + /** + * @brief Information of each step which the integrator decided to skip re-factoring the Jacobian. + * + */ + std::vector skip_lu_steps_; + /** + * @brief How many steps the integrator has taken. + * + */ + size_t num_steps_ = 0; + /** + * @brief Number of model residual function evaluations. + * + */ + size_t f_evals_ = 0; + /** + * @brief Number of model residual function evaluations which have been skipped by the integrator. + * + */ + size_t f_skipped_ = 0; + /** + * @brief Number of model Jacobian evaluations. + * + */ + size_t jac_evals_ = 0; + /** + * @brief Number of linear solves against the model Jacobian. + * + */ + size_t decomp_solves_ = 0; + /** + * @brief Minimum step size. + * + */ + RealT min_step_ = INFINITY; + /** + * @brief Maximum step size. + * + */ + RealT max_step_ = 0; + + std::string report() const; + Stats& operator+=(const Stats& other); + }; + + /** + * @brief Parameters of the integrator. + * + */ + struct Parameters + { + /** + * @brief What step size the first step should take. + * + * @todo Consider adding a starting step size selector to select this automatically. + */ + RealT starting_step_ = 1e-5; + /** + * @brief The maximum number of steps the integrator should take. If the integrator has not reached the final time before + * taking this many steps, then integration is stopped. For more details, see `integrate()`. + * + */ + size_t max_steps_ = 2000; + /** + * @brief Whether or not the integrator should attempt to skip Jacobian decompositions. + * + * @note This feature is only available if the underlying method is a Rosenbrock-W method and can speed up + * the time taken to compute each step. However, the overall number of steps taken will increase. + * + */ + bool skip_lu_ = false; + }; + + /** + * @brief A list of the coefficients needed to complete Rosenbrock integration in an accurate way. + * Each tableau can be considered a different method or scheme, and will have different properties, + * advantages, and disadvantages. + * + */ + struct Tableau + { + /** + * @brief The number of stages used by the method. Each stage requires one model residual evaluation. + * + */ + size_t num_stages_; + /** + * @brief The coefficient along the diagonal of the Gamma matrix. + * + */ + RealT gamma_; + /** + * @brief A vector of sums of rows of the alpha matrix. These are the classic + * Runge-Kutta 'c' coefficients, or abscissae. The size of this vector + * should be equal to `num_stages`. + * + */ + std::unique_ptr alpha_sum_; + /** + * @brief A vector of sums of rows of the Gamma matrix. The size of this vector + * should be equal to `num_stages`. + * + */ + std::unique_ptr gamma_sum_; + /** + * @brief A vector of weights for constructing the final solution from the stages. + * The size of this vector should be equal to `num_stages`. + * + */ + std::unique_ptr m_; + /** + * @brief OPTIONAL vector of coefficients for the embedded error method. If it exists, + * the size of this vector should be equal to `num_stages`. + * + */ + std::unique_ptr e_; + /** + * @brief The transformed A coefficient matrix. Strictly lower triangular and stored in dense row-major form. + * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. + * + */ + std::unique_ptr A_; + /** + * @brief The transformed C coefficient matrix. Strictly lower triangular and stored in dense row-major form. + * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. + * + */ + std::unique_ptr C_; + /** + * @brief OPTIONAL matrix of dense coefficients. Defines how the stages should be transformed into interpolant + * nodes for computing dense output. The interpolating polynomial has an order one less than the order of + * the method, and two interpolant nodes are already pre-computed, so if this matrix exists it should be + * `order` - 2 by `num_stages` large. + * + */ + std::unique_ptr H_; + /** + * @brief What ODE order these coefficients satisfy. If `is_dae` is true, then the coefficients must additionally satisfy + * DAE conditions up to this order. If `is_w` is true, then the coefficients must additionally satisfy ROW conditions + * up to this order. If `is_krylov` is true, then the coefficients must additionally satisfy ROK condition up to this order. + * + */ + + uint8_t order_; + /** + * @brief Whether or not these coefficients are appropriate to use in a Rosenbrock-Krylov (ROK) solver. + * + */ + bool is_krylov_; + /** + * @brief Whether or not these coefficients satisfy Rosenbrock-W (ROW) order conditions up to `order`. + * The integrator may take advantage of this fact by e.g. using time-delay Jacobians to speed up computation. + * + */ + bool is_w_; + /** + * @brief Whether or not these coefficients satisfy DAE order conditions up to `order`. If this is not true, + * these coefficients should not be used to solve models with algebraic conditions (indicated by a + * `Model::Evaluator::tag_` value of 0). + * + */ + bool is_dae_; + + /** + * @brief Whether or not this tableau contains an embedded error estimator method. + * + */ + constexpr bool hasEmbedded() const + { + return static_cast(e_); + } + + /** + * @brief Whether or not this tableau contains coefficients which can be used to construct dense output. + * + */ + constexpr bool hasDenseOutput() const + { + return static_cast(H_); + } - std::string csvReport() const; - std::string report() const; - }; + /** + * @brief Helper function for accessing elements of `A` + * + */ + constexpr RealT getA(size_t row, size_t col) const + { + return A_[row * num_stages_ + col]; + } - /** - * @brief Keeps track of running statistics of the integrator since the last `initializeSimulation()` call. - * - */ - struct Stats - { - /** - * @brief Information of each step which has been rejected. - * - */ - std::vector rejections_; - /** - * @brief Information of each step which the integrator decided to skip re-factoring the Jacobian. - * - */ - std::vector skip_lu_steps_; - /** - * @brief How many steps the integrator has taken. - * - */ - size_t num_steps_ = 0; - /** - * @brief Number of model residual function evaluations. - * - */ - size_t f_evals_ = 0; - /** - * @brief Number of model residual function evaluations which have been skipped by the integrator. - * - */ - size_t f_skipped_ = 0; - /** - * @brief Number of model Jacobian evaluations. - * - */ - size_t jac_evals_ = 0; - /** - * @brief Number of linear solves against the model Jacobian. - * - */ - size_t decomp_solves_ = 0; - /** - * @brief Minimum step size. - * - */ - RealT min_step_ = INFINITY; - /** - * @brief Maximum step size. - * - */ - RealT max_step_ = 0; + constexpr bool canReuseAsum(size_t stage) const; + constexpr bool canReuseAsumForOut() const; + constexpr std::optional errorEstimatorStage() const; - std::string report() const; - Stats& operator+=(const Stats& other); - }; + static Tableau linImplicitEuler(); + static Tableau rodas5p(); + }; - /** - * @brief Parameters of the integrator. - * - */ - struct Parameters - { + private: /** - * @brief What step size the first step should take. + * @brief The current step size of the integrator. If integration is ever stopped and resumed, this value will be used for + * the initial step after resuming. * - * @todo Consider adding a starting step size selector to select this automatically. */ - RealT starting_step_ = 1e-5; + RealT step_size_ = 0; /** - * @brief The maximum number of steps the integrator should take. If the integrator has not reached the final time before - * taking this many steps, then integration is stopped. For more details, see `integrate()`. + * @brief The step size of the previous step. Used for operations which need to be done on the current step, but step size + * control for the next step has already been performed. * */ - size_t max_steps_ = 2000; + RealT prev_step_size_ = 0; /** - * @brief Whether or not the integrator should attempt to skip Jacobian decompositions. - * - * @note This feature is only available if the underlying method is a Rosenbrock-W method and can speed up - * the time taken to compute each step. However, the overall number of steps taken will increase. + * @brief Whether or not the integrator should attempt to skip Jacobian decomposition on the next step. Controlled by the + * time stepping algorithm in \link integrate() \endlink . Generally, this should only be set if we suspect the Jacobian for the + * last step is a good enough approximation of the Jacobian on the next step. Non-ROW methods need exact Jacobians, + * so this should only be set for ROW methods, and when the step size for the next step is the same as the step size + * as the previous step. * */ - bool skip_lu_ = false; - }; - - /** - * @brief A list of the coefficients needed to complete Rosenbrock integration in an accurate way. - * Each tableau can be considered a different method or scheme, and will have different properties, - * advantages, and disadvantages. - * - */ - struct Tableau - { + bool skip_lu_ = false; /** - * @brief The number of stages used by the method. Each stage requires one model residual evaluation. + * @brief Whether or not the integrator should attempt to skip the residual function evaluation of the first stage on the + * next step. This should only be used when a step is rejected and the residual function is evaluated at the exact + * same arguments as the previous step. Then \ref RHS_first_stage_ can be re-used rather than re-calculated. * */ - size_t num_stages_; + bool skip_f_ = false; /** - * @brief The coefficient along the diagonal of the Gamma matrix. + * @brief Keeps track of whether or not the integrator currently has valid dense coefficients. + * i.e. they have been computed and haven't been invalidated by taking another step. This can be used to avoid + * re-computing dense coefficients when interpolating states multiple times in one step. * */ - RealT gamma_; + bool dense_coefficients_valid_ = false; + /** - * @brief A vector of sums of rows of the alpha matrix. These are the classic - * Runge-Kutta 'c' coefficients, or abscissae. The size of this vector - * should be equal to `num_stages`. + * @brief The tableau of Rosenbrock coefficients currently being used by the integrator. * */ - std::unique_ptr alpha_sum_; + Tableau tab_; /** - * @brief A vector of sums of rows of the Gamma matrix. The size of this vector - * should be equal to `num_stages`. + * @brief The model being simulated. * */ - std::unique_ptr gamma_sum_; + GridKit::Model::Evaluator* model_; /** - * @brief A vector of weights for constructing the final solution from the stages. - * The size of this vector should be equal to `num_stages`. + * @brief The linear solver to be used during integration in \link timeStep() \endlink. * */ - std::unique_ptr m_; + GridKit::LinearAlgebra::LinearSolver& lin_solver_; /** - * @brief OPTIONAL vector of coefficients for the embedded error method. If it exists, - * the size of this vector should be equal to `num_stages`. + * @brief The vector handler to be used for vector operations by the integrator. * */ - std::unique_ptr e_; + GridKit::LinearAlgebra::VectorHandler& vector_handler_; /** - * @brief The transformed A coefficient matrix. Strictly lower triangular and stored in dense row-major form. - * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. + * @brief The `ErrorNorm` to be used by the `StepController` in \link integrate() \endlink. * - */ - std::unique_ptr A_; - /** - * @brief The transformed C coefficient matrix. Strictly lower triangular and stored in dense row-major form. - * Upper triangular terms are not accessed. Should be `num_stages` by `num_stages` large. + * @note Can be `nullptr` if no `ErrorNorm` is configured, in case the `StepController` does not need an error to be calculated. * + * @todo Should be removed from the `Rosenbrock` class. Whether or not this is needed is dependent on the `StepController`, so it should be stored there. */ - std::unique_ptr C_; + const ErrorNorm* err_norm_; /** - * @brief OPTIONAL matrix of dense coefficients. Defines how the stages should be transformed into interpolant - * nodes for computing dense output. The interpolating polynomial has an order one less than the order of - * the method, and two interpolant nodes are already pre-computed, so if this matrix exists it should be - * `order` - 2 by `num_stages` large. + * @brief The memory space where linear algebra operations hsould be done in. * */ - std::unique_ptr H_; + GridKit::memory::MemorySpace memspace_; + /** - * @brief What ODE order these coefficients satisfy. If `is_dae` is true, then the coefficients must additionally satisfy - * DAE conditions up to this order. If `is_w` is true, then the coefficients must additionally satisfy ROW conditions - * up to this order. If `is_krylov` is true, then the coefficients must additionally satisfy ROK condition up to this order. + * @brief The current simulation time. * */ + RealT current_time_; - uint8_t order_; /** - * @brief Whether or not these coefficients are appropriate to use in a Rosenbrock-Krylov (ROK) solver. + * @brief The state from last step. * */ - bool is_krylov_; + std::unique_ptr y_prev_; /** - * @brief Whether or not these coefficients satisfy Rosenbrock-W (ROW) order conditions up to `order`. - * The integrator may take advantage of this fact by e.g. using time-delay Jacobians to speed up computation. + * @brief The state on the current step. * */ - bool is_w_; + std::unique_ptr y_cur_; /** - * @brief Whether or not these coefficients satisfy DAE order conditions up to `order`. If this is not true, - * these coefficients should not be used to solve models with algebraic conditions (indicated by a - * `Model::Evaluator::tag_` value of 0). + * @brief The incoming state for the next step. * */ - bool is_dae_; - + std::unique_ptr y_new_; /** - * @brief Whether or not this tableau contains an embedded error estimator method. + * @brief Used as output for dense output interpolation. * */ - constexpr bool hasEmbedded() const - { - return static_cast(e_); - } + std::unique_ptr y_interp_; /** - * @brief Whether or not this tableau contains coefficients which can be used to construct dense output. + * @brief Configured parameters for the integrator. * */ - constexpr bool hasDenseOutput() const - { - return static_cast(H_); - } + Parameters params_; /** - * @brief Helper function for accessing elements of `A` + * @brief Running statistics of the integrator. Reset by \link initializeSimulation() \endlink. * */ - constexpr RealT getA(size_t row, size_t col) const - { - return A_[row * num_stages_ + col]; - } - - constexpr bool canReuseAsum(size_t stage) const; - constexpr bool canReuseAsumForOut() const; - constexpr std::optional errorEstimatorStage() const; - - static Tableau linImplicitEuler(); - static Tableau rodas5p(); - }; - - private: - /** - * @brief The current step size of the integrator. If integration is ever stopped and resumed, this value will be used for - * the initial step after resuming. - * - */ - RealT step_size_ = 0; - /** - * @brief The step size of the previous step. Used for operations which need to be done on the current step, but step size - * control for the next step has already been performed. - * - */ - RealT prev_step_size_ = 0; - /** - * @brief Whether or not the integrator should attempt to skip Jacobian decomposition on the next step. Controlled by the - * time stepping algorithm in \link integrate() \endlink . Generally, this should only be set if we suspect the Jacobian for the - * last step is a good enough approximation of the Jacobian on the next step. Non-ROW methods need exact Jacobians, - * so this should only be set for ROW methods, and when the step size for the next step is the same as the step size - * as the previous step. - * - */ - bool skip_lu_ = false; - /** - * @brief Whether or not the integrator should attempt to skip the residual function evaluation of the first stage on the - * next step. This should only be used when a step is rejected and the residual function is evaluated at the exact - * same arguments as the previous step. Then \ref RHS_first_stage_ can be re-used rather than re-calculated. - * - */ - bool skip_f_ = false; - /** - * @brief Keeps track of whether or not the integrator currently has valid dense coefficients. - * i.e. they have been computed and haven't been invalidated by taking another step. This can be used to avoid - * re-computing dense coefficients when interpolating states multiple times in one step. - * - */ - bool dense_coefficients_valid_ = false; + Stats stats_; - /** - * @brief The tableau of Rosenbrock coefficients currently being used by the integrator. - * - */ - Tableau tab_; - /** - * @brief The model being simulated. - * - */ - GridKit::Model::Evaluator* model_; - /** - * @brief The linear solver to be used during integration in \link timeStep() \endlink. - * - */ - GridKit::LinearAlgebra::LinearSolver& lin_solver_; - /** - * @brief The vector handler to be used for vector operations by the integrator. - * - */ - GridKit::LinearAlgebra::VectorHandler& vector_handler_; - /** - * @brief The `ErrorNorm` to be used by the `StepController` in \link integrate() \endlink. - * - * @note Can be `nullptr` if no `ErrorNorm` is configured, in case the `StepController` does not need an error to be calculated. - * - * @todo Should be removed from the `Rosenbrock` class. Whether or not this is needed is dependent on the `StepController`, so it should be stored there. - */ - const ErrorNorm* err_norm_; - /** - * @brief The memory space where linear algebra operations hsould be done in. - * - */ - GridKit::memory::MemorySpace memspace_; + public: + Rosenbrock(Tableau&& tab, + GridKit::Model::Evaluator* model, + GridKit::LinearAlgebra::LinearSolver& lin_solver, + GridKit::LinearAlgebra::VectorHandler& vector_handler, + const ErrorNorm* err_norm, + GridKit::memory::MemorySpace memspace = GridKit::memory::HOST); - /** - * @brief The current simulation time. - * - */ - RealT current_time_; + [[nodiscard("May fail. Check error code.")]] + int allocate(); - /** - * @brief The state from last step. - * - */ - std::unique_ptr y_prev_; - /** - * @brief The state on the current step. - * - */ - std::unique_ptr y_cur_; - /** - * @brief The incoming state for the next step. - * - */ - std::unique_ptr y_new_; - /** - * @brief Used as output for dense output interpolation. - * - */ - std::unique_ptr y_interp_; + [[nodiscard("May fail. Check error code.")]] + int initializeSimulation(RealT t0); - /** - * @brief Configured parameters for the integrator. - * - */ - Parameters params_; + [[nodiscard("May fail. Check error code.")]] + int integrate(const std::vector& out_times, + StepController& step_controller, + Parameters params = {}, + std::optional> out_cb = {}, + std::optional> step_cb = {}); - /** - * @brief Running statistics of the integrator. Reset by \link initializeSimulation() \endlink. - * - */ - Stats stats_; - - public: - Rosenbrock(Tableau&& tab, - GridKit::Model::Evaluator* model, - GridKit::LinearAlgebra::LinearSolver& lin_solver, - GridKit::LinearAlgebra::VectorHandler& vector_handler, - const ErrorNorm* err_norm, - GridKit::memory::MemorySpace memspace = GridKit::memory::HOST); - - [[nodiscard("May fail. Check error code.")]] - int allocate(); - - [[nodiscard("May fail. Check error code.")]] - int initializeSimulation(RealT t0); - - [[nodiscard("May fail. Check error code.")]] - int integrate(const std::vector& out_times, - StepController& step_controller, - Parameters params = {}, - std::optional> out_cb = {}, - std::optional> step_cb = {}); - - private: - /** - * @brief A workspace used by `time_step` for the necessary linear algebra computations. - * - */ - mutable struct - { - /** - * @brief Sum of A-weighted states used for function evaluation in a stage. - * - */ - std::unique_ptr asum_; - /** - * @brief Sum of C-weighted states used in linearization in a stage. - * - */ - std::unique_ptr csum_; + private: /** - * @brief Right-hand side of linear solve used in a stage. + * @brief A workspace used by `time_step` for the necessary linear algebra computations. * */ - std::unique_ptr RHS_; - /** - * @brief Right-hand side of linear solve used in the first stage. - * Stored separately from \ref RHS_ since it can be re-used by future steps if the current step is rejected. - * - * @see `skip_f_` - * - */ - std::unique_ptr RHS_first_stage_; - /** - * @brief Time Jacobian for non-autonomous models. Currently unused, since models are assumed to be autonomous. - * - */ - std::unique_ptr dFdt_; - /** - * @brief Vector representation of a diagonal mass matrix. - * - */ - std::unique_ptr mass_; - /** - * @brief Estimated error produced by a step in a method with an empbedded error estimator. - * - * @see `Tableau::e` - * - */ - std::unique_ptr err_est_; - - /** - * @brief Whether or not the Jacobian has been factorized. Initial factorization is required, - * but pivots can be re-used for a refactorization later. Re-factorization can introduce - * errors when the Jacobian differs significantly, so this flag should be reset when - * a significant event can happen, such as re-initializing the simulation. - * - * @see \link initializeSimulation() \endlink - * - */ - bool jacobian_analyzed_ = false; - - /** - * @brief Integration stages - each a low-order approximation of the state at the abscissae given by - * the tableau. - * - */ - std::unique_ptr[]> stages_; - } workspace_; - - /** - * @brief Dense output interpolation nodes. Used to generate output states in-between steps. - * - * @see \link calcDenseCoeff() \endlink, \link interpDense() \endlink - * - */ - std::unique_ptr[]> dense_coeff_; - - public: - [[nodiscard("May fail. Check error code.")]] - int timeStep(RealT t0, RealT dt); - - State& errorEstimate() const; - - [[nodiscard("May fail. Check error code.")]] - int calcDenseCoeff(); - - [[nodiscard("May fail. Check error code.")]] - int interpDense(RealT theta); - }; -} // namespace Integrator + mutable struct + { + /** + * @brief Sum of A-weighted states used for function evaluation in a stage. + * + */ + std::unique_ptr asum_; + /** + * @brief Sum of C-weighted states used in linearization in a stage. + * + */ + std::unique_ptr csum_; + /** + * @brief Right-hand side of linear solve used in a stage. + * + */ + std::unique_ptr RHS_; + /** + * @brief Right-hand side of linear solve used in the first stage. + * Stored separately from \ref RHS_ since it can be re-used by future steps if the current step is rejected. + * + * @see `skip_f_` + * + */ + std::unique_ptr RHS_first_stage_; + /** + * @brief Time Jacobian for non-autonomous models. Currently unused, since models are assumed to be autonomous. + * + */ + std::unique_ptr dFdt_; + /** + * @brief Vector representation of a diagonal mass matrix. + * + */ + std::unique_ptr mass_; + /** + * @brief Estimated error produced by a step in a method with an empbedded error estimator. + * + * @see `Tableau::e` + * + */ + std::unique_ptr err_est_; + + /** + * @brief Whether or not the Jacobian has been factorized. Initial factorization is required, + * but pivots can be re-used for a refactorization later. Re-factorization can introduce + * errors when the Jacobian differs significantly, so this flag should be reset when + * a significant event can happen, such as re-initializing the simulation. + * + * @see \link initializeSimulation() \endlink + * + */ + bool jacobian_analyzed_ = false; + + /** + * @brief Integration stages - each a low-order approximation of the state at the abscissae given by + * the tableau. + * + */ + std::unique_ptr[]> stages_; + } workspace_; + + /** + * @brief Dense output interpolation nodes. Used to generate output states in-between steps. + * + * @see \link calcDenseCoeff() \endlink, \link interpDense() \endlink + * + */ + std::unique_ptr[]> dense_coeff_; + + public: + [[nodiscard("May fail. Check error code.")]] + int timeStep(RealT t0, RealT dt); + + State& errorEstimate() const; + + [[nodiscard("May fail. Check error code.")]] + int calcDenseCoeff(); + + [[nodiscard("May fail. Check error code.")]] + int interpDense(RealT theta); + }; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp index 75d7be825..4fd952bc2 100644 --- a/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp +++ b/GridKit/Solver/Dynamic/RosenbrockTableaus.cpp @@ -1,202 +1,205 @@ #include "Rosenbrock.hpp" -namespace Integrator +namespace AnalysisManager { - template - Rosenbrock::Tableau Rosenbrock::Tableau::linImplicitEuler() + namespace NativeDynamicSolver { - constexpr size_t num_stages = 1; - - Tableau re = { - .num_stages_ = num_stages, - .gamma_ = 1.0, - .alpha_sum_ = std::make_unique_for_overwrite(num_stages), - .gamma_sum_ = std::make_unique_for_overwrite(num_stages), - .m_ = std::make_unique_for_overwrite(num_stages), - .e_ = {}, - .A_ = std::make_unique_for_overwrite(num_stages * num_stages), - .C_ = std::make_unique_for_overwrite(num_stages * num_stages), - .H_ = {}, - .order_ = 1, - .is_krylov_ = true, - .is_w_ = false, - .is_dae_ = true, - }; - - re.alpha_sum_[0] = 0.0; - - re.gamma_sum_[0] = 1.0; - - re.m_[0] = 1.0; - - re.A_[0] = 0.0; - - re.C_[0] = 2.0; - - return re; - } - - /** - * @brief A 5th order Rosenbrock method suitable for solving parabolic PDEs. Created in \cite steinebach2023construction. - * - * - */ - template - Rosenbrock::Tableau Rosenbrock::Tableau::rodas5p() - { - constexpr size_t num_stages = 8; - - Tableau re = { - .num_stages_ = num_stages, - .gamma_ = 0.21193756319429014, - .alpha_sum_ = std::make_unique_for_overwrite(num_stages), - .gamma_sum_ = std::make_unique_for_overwrite(num_stages), - .m_ = std::make_unique_for_overwrite(num_stages), - .e_ = std::make_unique_for_overwrite(num_stages), - .A_ = std::make_unique_for_overwrite(num_stages * num_stages), - .C_ = std::make_unique_for_overwrite(num_stages * num_stages), - .H_ = std::make_unique_for_overwrite(num_stages * 3), - .order_ = 5, - .is_krylov_ = false, - .is_w_ = false, - .is_dae_ = true, - }; - - re.alpha_sum_[0] = 0.0; - re.alpha_sum_[1] = 0.6358126895828704; - re.alpha_sum_[2] = 0.4095798393397535; - re.alpha_sum_[3] = 0.9769306725060716; - re.alpha_sum_[4] = 0.4288403609558664; - re.alpha_sum_[5] = 1.0; - re.alpha_sum_[6] = 1.0; - re.alpha_sum_[7] = 1.0; - - re.gamma_sum_[0] = 0.21193756319429014; - re.gamma_sum_[1] = -0.42387512638858027; - re.gamma_sum_[2] = -0.3384627126235924; - re.gamma_sum_[3] = 1.8046452872882734; - re.gamma_sum_[4] = 2.325825639765069; - re.gamma_sum_[5] = 0.0; - re.gamma_sum_[6] = 0.0; - re.gamma_sum_[7] = 0.0; - - re.m_[0] = -7.502846399306121; - re.m_[1] = 2.561846144803919; - re.m_[2] = -11.627539656261098; - re.m_[3] = -0.18268767659942256; - re.m_[4] = 0.030198172008377946; - re.m_[5] = 1.0; - re.m_[6] = 1.0; - re.m_[7] = 1.0; - - re.e_[0] = 0.0; - re.e_[1] = 0.0; - re.e_[2] = 0.0; - re.e_[3] = 0.0; - re.e_[4] = 0.0; - re.e_[5] = 0.0; - re.e_[6] = 0.0; - re.e_[7] = 1.0; - - re.A_[1 * 8 + 0] = 3.0; - - re.A_[2 * 8 + 0] = 2.849394379747939; - re.A_[2 * 8 + 1] = 0.45842242204463923; - - re.A_[3 * 8 + 0] = -6.954028509809101; - re.A_[3 * 8 + 1] = 2.489845061869568; - re.A_[3 * 8 + 2] = -10.358996098473584; - - re.A_[4 * 8 + 0] = 2.8029986275628964; - re.A_[4 * 8 + 1] = 0.5072464736228206; - re.A_[4 * 8 + 2] = -0.3988312541770524; - re.A_[4 * 8 + 3] = -0.04721187230404641; - - re.A_[5 * 8 + 0] = -7.502846399306121; - re.A_[5 * 8 + 1] = 2.561846144803919; - re.A_[5 * 8 + 2] = -11.627539656261098; - re.A_[5 * 8 + 3] = -0.18268767659942256; - re.A_[5 * 8 + 4] = 0.030198172008377946; - - re.A_[6 * 8 + 0] = -7.502846399306121; - re.A_[6 * 8 + 1] = 2.561846144803919; - re.A_[6 * 8 + 2] = -11.627539656261098; - re.A_[6 * 8 + 3] = -0.18268767659942256; - re.A_[6 * 8 + 4] = 0.030198172008377946; - re.A_[6 * 8 + 5] = 1.0; - - re.A_[7 * 8 + 0] = -7.502846399306121; - re.A_[7 * 8 + 1] = 2.561846144803919; - re.A_[7 * 8 + 2] = -11.627539656261098; - re.A_[7 * 8 + 3] = -0.18268767659942256; - re.A_[7 * 8 + 4] = 0.030198172008377946; - re.A_[7 * 8 + 5] = 1.0; - re.A_[7 * 8 + 6] = 1.0; - - re.C_[1 * 8 + 0] = -14.155112264123755; - - re.C_[2 * 8 + 0] = -17.97296035885952; - re.C_[2 * 8 + 1] = -2.859693295451294; - - re.C_[3 * 8 + 0] = 147.12150275711716; - re.C_[3 * 8 + 1] = -1.41221402718213; - re.C_[3 * 8 + 2] = 71.68940251302358; - - re.C_[4 * 8 + 0] = 165.43517024871676; - re.C_[4 * 8 + 1] = -0.4592823456491126; - re.C_[4 * 8 + 2] = 42.90938336958603; - re.C_[4 * 8 + 3] = -5.961986721573306; - - re.C_[5 * 8 + 0] = 24.854864614690072; - re.C_[5 * 8 + 1] = -3.0009227002832186; - re.C_[5 * 8 + 2] = 47.4931110020768; - re.C_[5 * 8 + 3] = 5.5814197821558125; - re.C_[5 * 8 + 4] = -0.6610691825249471; - - re.C_[6 * 8 + 0] = 30.91273214028599; - re.C_[6 * 8 + 1] = -3.1208243349937974; - re.C_[6 * 8 + 2] = 77.79954646070892; - re.C_[6 * 8 + 3] = 34.28646028294783; - re.C_[6 * 8 + 4] = -19.097331116725623; - re.C_[6 * 8 + 5] = -28.087943162872662; - - re.C_[7 * 8 + 0] = 37.80277123390563; - re.C_[7 * 8 + 1] = -3.2571969029072276; - re.C_[7 * 8 + 2] = 112.26918849496327; - re.C_[7 * 8 + 3] = 66.9347231244047; - re.C_[7 * 8 + 4] = -40.06618937091002; - re.C_[7 * 8 + 5] = -54.66780262877968; - re.C_[7 * 8 + 6] = -9.48861652309627; - - re.H_[0 * 8 + 0] = 25.948786856663858; - re.H_[0 * 8 + 1] = -2.5579724845846235; - re.H_[0 * 8 + 2] = 10.433815404888879; - re.H_[0 * 8 + 3] = -2.3679251022685204; - re.H_[0 * 8 + 4] = 0.524948541321073; - re.H_[0 * 8 + 5] = 1.1241088310450404; - re.H_[0 * 8 + 6] = 0.4272876194431874; - re.H_[0 * 8 + 7] = -0.17202221070155493; - - re.H_[1 * 8 + 0] = -9.91568850695171; - re.H_[1 * 8 + 1] = -0.9689944594115154; - re.H_[1 * 8 + 2] = 3.0438037242978453; - re.H_[1 * 8 + 3] = -24.495224566215796; - re.H_[1 * 8 + 4] = 20.176138334709044; - re.H_[1 * 8 + 5] = 15.98066361424651; - re.H_[1 * 8 + 6] = -6.789040303419874; - re.H_[1 * 8 + 7] = -6.710236069923372; - - re.H_[2 * 8 + 0] = 11.419903575922262; - re.H_[2 * 8 + 1] = 2.8879645146136994; - re.H_[2 * 8 + 2] = 72.92137995996029; - re.H_[2 * 8 + 3] = 80.12511834622643; - re.H_[2 * 8 + 4] = -52.072871366152654; - re.H_[2 * 8 + 5] = -59.78993625266729; - re.H_[2 * 8 + 6] = -0.15582684282751913; - re.H_[2 * 8 + 7] = 4.883087185713722; - - return re; - } - - template class Rosenbrock; -} // namespace Integrator + template + Rosenbrock::Tableau Rosenbrock::Tableau::linImplicitEuler() + { + constexpr size_t num_stages = 1; + + Tableau re = { + .num_stages_ = num_stages, + .gamma_ = 1.0, + .alpha_sum_ = std::make_unique_for_overwrite(num_stages), + .gamma_sum_ = std::make_unique_for_overwrite(num_stages), + .m_ = std::make_unique_for_overwrite(num_stages), + .e_ = {}, + .A_ = std::make_unique_for_overwrite(num_stages * num_stages), + .C_ = std::make_unique_for_overwrite(num_stages * num_stages), + .H_ = {}, + .order_ = 1, + .is_krylov_ = true, + .is_w_ = false, + .is_dae_ = true, + }; + + re.alpha_sum_[0] = 0.0; + + re.gamma_sum_[0] = 1.0; + + re.m_[0] = 1.0; + + re.A_[0] = 0.0; + + re.C_[0] = 2.0; + + return re; + } + + /** + * @brief A 5th order Rosenbrock method suitable for solving parabolic PDEs. Created in \cite steinebach2023construction. + * + * + */ + template + Rosenbrock::Tableau Rosenbrock::Tableau::rodas5p() + { + constexpr size_t num_stages = 8; + + Tableau re = { + .num_stages_ = num_stages, + .gamma_ = 0.21193756319429014, + .alpha_sum_ = std::make_unique_for_overwrite(num_stages), + .gamma_sum_ = std::make_unique_for_overwrite(num_stages), + .m_ = std::make_unique_for_overwrite(num_stages), + .e_ = std::make_unique_for_overwrite(num_stages), + .A_ = std::make_unique_for_overwrite(num_stages * num_stages), + .C_ = std::make_unique_for_overwrite(num_stages * num_stages), + .H_ = std::make_unique_for_overwrite(num_stages * 3), + .order_ = 5, + .is_krylov_ = false, + .is_w_ = false, + .is_dae_ = true, + }; + + re.alpha_sum_[0] = 0.0; + re.alpha_sum_[1] = 0.6358126895828704; + re.alpha_sum_[2] = 0.4095798393397535; + re.alpha_sum_[3] = 0.9769306725060716; + re.alpha_sum_[4] = 0.4288403609558664; + re.alpha_sum_[5] = 1.0; + re.alpha_sum_[6] = 1.0; + re.alpha_sum_[7] = 1.0; + + re.gamma_sum_[0] = 0.21193756319429014; + re.gamma_sum_[1] = -0.42387512638858027; + re.gamma_sum_[2] = -0.3384627126235924; + re.gamma_sum_[3] = 1.8046452872882734; + re.gamma_sum_[4] = 2.325825639765069; + re.gamma_sum_[5] = 0.0; + re.gamma_sum_[6] = 0.0; + re.gamma_sum_[7] = 0.0; + + re.m_[0] = -7.502846399306121; + re.m_[1] = 2.561846144803919; + re.m_[2] = -11.627539656261098; + re.m_[3] = -0.18268767659942256; + re.m_[4] = 0.030198172008377946; + re.m_[5] = 1.0; + re.m_[6] = 1.0; + re.m_[7] = 1.0; + + re.e_[0] = 0.0; + re.e_[1] = 0.0; + re.e_[2] = 0.0; + re.e_[3] = 0.0; + re.e_[4] = 0.0; + re.e_[5] = 0.0; + re.e_[6] = 0.0; + re.e_[7] = 1.0; + + re.A_[1 * 8 + 0] = 3.0; + + re.A_[2 * 8 + 0] = 2.849394379747939; + re.A_[2 * 8 + 1] = 0.45842242204463923; + + re.A_[3 * 8 + 0] = -6.954028509809101; + re.A_[3 * 8 + 1] = 2.489845061869568; + re.A_[3 * 8 + 2] = -10.358996098473584; + + re.A_[4 * 8 + 0] = 2.8029986275628964; + re.A_[4 * 8 + 1] = 0.5072464736228206; + re.A_[4 * 8 + 2] = -0.3988312541770524; + re.A_[4 * 8 + 3] = -0.04721187230404641; + + re.A_[5 * 8 + 0] = -7.502846399306121; + re.A_[5 * 8 + 1] = 2.561846144803919; + re.A_[5 * 8 + 2] = -11.627539656261098; + re.A_[5 * 8 + 3] = -0.18268767659942256; + re.A_[5 * 8 + 4] = 0.030198172008377946; + + re.A_[6 * 8 + 0] = -7.502846399306121; + re.A_[6 * 8 + 1] = 2.561846144803919; + re.A_[6 * 8 + 2] = -11.627539656261098; + re.A_[6 * 8 + 3] = -0.18268767659942256; + re.A_[6 * 8 + 4] = 0.030198172008377946; + re.A_[6 * 8 + 5] = 1.0; + + re.A_[7 * 8 + 0] = -7.502846399306121; + re.A_[7 * 8 + 1] = 2.561846144803919; + re.A_[7 * 8 + 2] = -11.627539656261098; + re.A_[7 * 8 + 3] = -0.18268767659942256; + re.A_[7 * 8 + 4] = 0.030198172008377946; + re.A_[7 * 8 + 5] = 1.0; + re.A_[7 * 8 + 6] = 1.0; + + re.C_[1 * 8 + 0] = -14.155112264123755; + + re.C_[2 * 8 + 0] = -17.97296035885952; + re.C_[2 * 8 + 1] = -2.859693295451294; + + re.C_[3 * 8 + 0] = 147.12150275711716; + re.C_[3 * 8 + 1] = -1.41221402718213; + re.C_[3 * 8 + 2] = 71.68940251302358; + + re.C_[4 * 8 + 0] = 165.43517024871676; + re.C_[4 * 8 + 1] = -0.4592823456491126; + re.C_[4 * 8 + 2] = 42.90938336958603; + re.C_[4 * 8 + 3] = -5.961986721573306; + + re.C_[5 * 8 + 0] = 24.854864614690072; + re.C_[5 * 8 + 1] = -3.0009227002832186; + re.C_[5 * 8 + 2] = 47.4931110020768; + re.C_[5 * 8 + 3] = 5.5814197821558125; + re.C_[5 * 8 + 4] = -0.6610691825249471; + + re.C_[6 * 8 + 0] = 30.91273214028599; + re.C_[6 * 8 + 1] = -3.1208243349937974; + re.C_[6 * 8 + 2] = 77.79954646070892; + re.C_[6 * 8 + 3] = 34.28646028294783; + re.C_[6 * 8 + 4] = -19.097331116725623; + re.C_[6 * 8 + 5] = -28.087943162872662; + + re.C_[7 * 8 + 0] = 37.80277123390563; + re.C_[7 * 8 + 1] = -3.2571969029072276; + re.C_[7 * 8 + 2] = 112.26918849496327; + re.C_[7 * 8 + 3] = 66.9347231244047; + re.C_[7 * 8 + 4] = -40.06618937091002; + re.C_[7 * 8 + 5] = -54.66780262877968; + re.C_[7 * 8 + 6] = -9.48861652309627; + + re.H_[0 * 8 + 0] = 25.948786856663858; + re.H_[0 * 8 + 1] = -2.5579724845846235; + re.H_[0 * 8 + 2] = 10.433815404888879; + re.H_[0 * 8 + 3] = -2.3679251022685204; + re.H_[0 * 8 + 4] = 0.524948541321073; + re.H_[0 * 8 + 5] = 1.1241088310450404; + re.H_[0 * 8 + 6] = 0.4272876194431874; + re.H_[0 * 8 + 7] = -0.17202221070155493; + + re.H_[1 * 8 + 0] = -9.91568850695171; + re.H_[1 * 8 + 1] = -0.9689944594115154; + re.H_[1 * 8 + 2] = 3.0438037242978453; + re.H_[1 * 8 + 3] = -24.495224566215796; + re.H_[1 * 8 + 4] = 20.176138334709044; + re.H_[1 * 8 + 5] = 15.98066361424651; + re.H_[1 * 8 + 6] = -6.789040303419874; + re.H_[1 * 8 + 7] = -6.710236069923372; + + re.H_[2 * 8 + 0] = 11.419903575922262; + re.H_[2 * 8 + 1] = 2.8879645146136994; + re.H_[2 * 8 + 2] = 72.92137995996029; + re.H_[2 * 8 + 3] = 80.12511834622643; + re.H_[2 * 8 + 4] = -52.072871366152654; + re.H_[2 * 8 + 5] = -59.78993625266729; + re.H_[2 * 8 + 6] = -0.15582684282751913; + re.H_[2 * 8 + 7] = 4.883087185713722; + + return re; + } + + template class Rosenbrock; + } // namespace NativeDynamicSolver +} // namespace AnalysisManager diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index 6d464732d..7f4dd3cf6 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -335,7 +335,7 @@ namespace GridKit template class RosenbrockTests { - using Rosenbrock = Integrator::Rosenbrock; + using Rosenbrock = AnalysisManager::NativeDynamicSolver::Rosenbrock; using RealT = typename GridKit::ScalarTraits::RealT; public: @@ -363,7 +363,7 @@ namespace GridKit return success.report(__func__); } - Integrator::FixedStep step_controller; + AnalysisManager::NativeDynamicSolver::FixedStep step_controller; double final_time = 2.0; std::vector out_times = {final_time}; diff --git a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp index 93debd641..c311c2e8e 100644 --- a/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp +++ b/tests/UnitTests/Solver/Dynamic/runRosenbrockTests.cpp @@ -6,12 +6,13 @@ int main() { using namespace GridKit; using namespace GridKit::Testing; + using namespace AnalysisManager::NativeDynamicSolver; GridKit::Testing::TestingResults result; GridKit::Testing::RosenbrockTests test; - result += test.test_order(Integrator::Rosenbrock::Tableau::linImplicitEuler(), -1.0, -5.0); - result += test.test_order(Integrator::Rosenbrock::Tableau::rodas5p(), -0.5, -2.5); + result += test.test_order(Rosenbrock::Tableau::linImplicitEuler(), -1.0, -5.0); + result += test.test_order(Rosenbrock::Tableau::rodas5p(), -0.5, -2.5); return result.summary(); } From 774f47de86fd759712152d2097ac55d40360854c Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 14 Jul 2026 14:05:09 -0400 Subject: [PATCH 52/57] Remove ResolveMemoryUtils.hpp This was an unused artifact - everything has been moved to the ResolveSystemSolver. --- .../MemoryUtilities/ResolveMemoryUtils.hpp | 28 ------------------- GridKit/Solver/Dynamic/Rosenbrock.cpp | 1 - 2 files changed, 29 deletions(-) delete mode 100644 GridKit/MemoryUtilities/ResolveMemoryUtils.hpp diff --git a/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp b/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp deleted file mode 100644 index 8685f90b5..000000000 --- a/GridKit/MemoryUtilities/ResolveMemoryUtils.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -#include - -namespace GridKit -{ - namespace memory - { - /** - * @brief Converts a GridKit \ref MemorySpace to its corresponding Re::Solve MemorySpace - * - */ - inline ReSolve::memory::MemorySpace memorySpaceAsResolve(MemorySpace memspace) - { - switch (memspace) - { - case HOST: - return ReSolve::memory::HOST; - case DEVICE: - return ReSolve::memory::DEVICE; - default: - throw "Memory space not supported"; - } - } - } // namespace memory -} // namespace GridKit diff --git a/GridKit/Solver/Dynamic/Rosenbrock.cpp b/GridKit/Solver/Dynamic/Rosenbrock.cpp index af3ad984b..8978bcd91 100644 --- a/GridKit/Solver/Dynamic/Rosenbrock.cpp +++ b/GridKit/Solver/Dynamic/Rosenbrock.cpp @@ -6,7 +6,6 @@ #include #include -#include /** * @brief A small helper macro to "bubble" errors. The Rosenbrock implementations call many From 2878373f0de4cdb0a7d28407af20ffb6da3aa180 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 14 Jul 2026 15:17:16 -0400 Subject: [PATCH 53/57] document test_order --- .../Solver/Dynamic/RosenbrockTests.hpp | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp index 7f4dd3cf6..49ac22649 100644 --- a/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp +++ b/tests/UnitTests/Solver/Dynamic/RosenbrockTests.hpp @@ -21,6 +21,14 @@ namespace GridKit { namespace Model { + /** + * @brief A test DAE (referred to as the "trigonometric DAE") that is useful for evaluating order + * of DAE integrators. The system is 2-dimensional, with one differential variable and one algebraic variable. + * The DAE is index 1 and the derivatives of the model are non-vanishing. + * + * The valid simulation time interval is \f([0.5,2]\f) and the model is initialized at \f(t = 0.5\f). + * + */ template class TrigonometricDaeEvaluator : public Model::Evaluator { @@ -339,12 +347,26 @@ namespace GridKit using RealT = typename GridKit::ScalarTraits::RealT; public: + /** + * @brief Test a Rosenbrock tableau by verifying its order empirically. Empirical order is calculated + * by running a convergence test on the problem modeled in \ref Model::TrigonometricDaeEvaluator. 21 + * simulations are run on the problem with a fixed-step step size controller with a number of steps + * logarithmically distributed in \f(\left[10^a, 10^b\right]\f) where \f(a\f) is `step_exponent_lower` + * and \f(b\f) is `step_exponent_upper`. The average slope between pairs of simulations is taken to be + * the empirical order. The test succeeds if the empirical order is at least 85% of the expected theoretical order. + * + * @param tab The tableau to test + * @param step_exponent_lower The exponent describing the smallest number of steps to take during a simulation. + * @param step_exponent_upper The exponent describing the largest number of steps to take during a simulation. + */ TestOutcome test_order(Rosenbrock::Tableau&& tab, double step_exponent_lower, double step_exponent_upper) { TestStatus success = true; + // Tableaus keep track of their theoretical order. We will attempt to match the empirical order to this. uint8_t expected_order = tab.order_; + // Setup the model, linear solver, and integrator Model::TrigonometricDaeEvaluator model; model.allocate(); model.initialize(); @@ -365,32 +387,44 @@ namespace GridKit AnalysisManager::NativeDynamicSolver::FixedStep step_controller; - double final_time = 2.0; - std::vector out_times = {final_time}; - + // The number of simulations to run to calculate empirical order. Must be at least 2, + // since empirical order is calculated pairwise between simulations. size_t num_samples = 21; + // Output vectors to keep track of data used to calculate empirical order. + // Each simulation will record its step size and final error. std::vector step_sizes; std::vector errors; - auto out_cb = [&]([[maybe_unused]] double t) + // An output callback to populate step_sizes and errors. Each simulation will call this callback + // at the end of the simulation (final_time). It will then calculate the final error by comparing the + // solution of the simulation to the true answer. + double final_time = 2.0; + std::vector out_times = {final_time}; + auto out_cb = [&]([[maybe_unused]] double t) { double error = 0.0; double sol_norm = 0.0; + // The final solution of the simulation const std::vector& state = model.y(); + // The difference from the simulated solution to the true solution error += std::pow(state[0] - sinh(final_time), 2); error += std::pow(state[1] - tanh(final_time), 2); sol_norm += std::pow(sinh(final_time), 2); sol_norm += std::pow(tanh(final_time), 2); + // Error relative to the true solution errors.push_back(std::sqrt(error) / std::sqrt(sol_norm)); }; + // Perform all of the simulations and populate step_sizes and errors using out_cb for (size_t i = 0; i < num_samples; i++) { + // Logarithmically distribute step_size based on step_exponent_lower and step_exponent_upper. + // Round it to ensure num_steps is integral (do not invoke the dense output, which can add additional errors). double step_size = std::pow(10, step_exponent_lower + static_cast(i) * (step_exponent_upper - step_exponent_lower) / static_cast(num_samples - 1)); double num_steps = round((final_time - 0.5) / step_size); step_size = (final_time - 0.5) / num_steps; @@ -413,6 +447,7 @@ namespace GridKit } } + // Print output data std::cerr << "Step sizes\n"; for (double step_size : step_sizes) { @@ -426,6 +461,7 @@ namespace GridKit } std::cerr << "\n\n"; + // Calculate empirical order. Each pairwise order is calculated, then averaged. std::vector pairwise_orders; double empirical_order = 0.0; for (size_t i = 1; i < num_samples; i++) @@ -435,6 +471,7 @@ namespace GridKit } empirical_order /= static_cast(num_samples - 1); + // Print test result - observed empirical order and the expected order. std::cerr << "Empirical order: " << std::fixed << std::setprecision(5) << empirical_order << " v.s. expected " << static_cast(expected_order) << '\n'; success *= empirical_order > expected_order * 0.85; From 91d0fd8b728433709696bd075ac2f6f4b1c496db Mon Sep 17 00:00:00 2001 From: alexander-novo Date: Tue, 14 Jul 2026 19:17:44 +0000 Subject: [PATCH 54/57] Apply pre-commit fixes --- GridKit/Solver/Dynamic/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index dd4e87268..0e91e4d9e 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -59,4 +59,4 @@ if(GRIDKIT_ENABLE_RESOLVE) GridKit::sparse_matrix) endif() -add_subdirectory(Native) \ No newline at end of file +add_subdirectory(Native) From e4b1206d87d6b01ae6b1657c847b9087baa10359 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Tue, 14 Jul 2026 15:31:21 -0400 Subject: [PATCH 55/57] Add necessary headers to cmake --- GridKit/Solver/Dynamic/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index 0e91e4d9e..c881e8f41 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -9,7 +9,7 @@ if(GRIDKIT_ENABLE_SUNDIALS_SPARSE) gridkit_add_library( solvers_dyn SOURCES Ida.cpp - HEADERS Ida.hpp + HEADERS Ida.hpp DynamicSolver.hpp LINK_LIBRARIES PUBLIC SUNDIALS::nvecserial @@ -27,7 +27,7 @@ else() gridkit_add_library( solvers_dyn SOURCES Ida.cpp - HEADERS Ida.hpp + HEADERS Ida.hpp DynamicSolver.hpp LINK_LIBRARIES PUBLIC SUNDIALS::nvecserial @@ -45,7 +45,7 @@ if(GRIDKIT_ENABLE_RESOLVE) gridkit_add_library( rosenbrock SOURCES Rosenbrock.cpp RosenbrockTableaus.cpp - HEADERS Rosenbrock.hpp + HEADERS Rosenbrock.hpp Native/ErrorNorm.hpp Native/StepControl.hpp Native/StepController.hpp LINK_LIBRARIES PRIVATE ReSolve::ReSolve From bcfc4c52451833d52af154f81950b765fae696c8 Mon Sep 17 00:00:00 2001 From: alexander-novo Date: Tue, 14 Jul 2026 19:31:43 +0000 Subject: [PATCH 56/57] Apply pre-commit fixes --- GridKit/Solver/Dynamic/CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GridKit/Solver/Dynamic/CMakeLists.txt b/GridKit/Solver/Dynamic/CMakeLists.txt index c881e8f41..e46292e41 100644 --- a/GridKit/Solver/Dynamic/CMakeLists.txt +++ b/GridKit/Solver/Dynamic/CMakeLists.txt @@ -45,7 +45,10 @@ if(GRIDKIT_ENABLE_RESOLVE) gridkit_add_library( rosenbrock SOURCES Rosenbrock.cpp RosenbrockTableaus.cpp - HEADERS Rosenbrock.hpp Native/ErrorNorm.hpp Native/StepControl.hpp Native/StepController.hpp + HEADERS Rosenbrock.hpp + Native/ErrorNorm.hpp + Native/StepControl.hpp + Native/StepController.hpp LINK_LIBRARIES PRIVATE ReSolve::ReSolve From 89412959b86f401dcb3f17da06943b697a53db7d Mon Sep 17 00:00:00 2001 From: pelesh Date: Wed, 15 Jul 2026 21:02:33 -0400 Subject: [PATCH 57/57] Fix compile error. --- GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp b/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp index 003bb8281..23cc0f306 100644 --- a/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp +++ b/GridKit/Solver/Dynamic/Native/AdaptiveStep.cpp @@ -1,5 +1,6 @@ #include "AdaptiveStep.hpp" +#include #include #include