diff --git a/Code/Source/solver/CMakeLists.txt b/Code/Source/solver/CMakeLists.txt index c546c2822..30294bf16 100644 --- a/Code/Source/solver/CMakeLists.txt +++ b/Code/Source/solver/CMakeLists.txt @@ -156,7 +156,8 @@ set(CSRCS PetscLinearAlgebra.h PetscLinearAlgebra.cpp TrilinosLinearAlgebra.h TrilinosLinearAlgebra.cpp Tensor4.h Tensor4.cpp - Vector.h Vector.cpp + Vector.h Vector.cpp + fourier_interpolation.cpp lapack_defs.h diff --git a/Code/Source/solver/CmMod.cpp b/Code/Source/solver/CmMod.cpp index f3485a2bb..2fbfb1c86 100644 --- a/Code/Source/solver/CmMod.cpp +++ b/Code/Source/solver/CmMod.cpp @@ -138,6 +138,11 @@ void cmType::bcast(const CmMod& cm_mod, Vector& data) const MPI_Bcast(data.data(), data.size(), cm_mod::mpint, cm_mod.master, com()); } +/// @brief bcast unsigned int +void cmType::bcast(const CmMod &cm_mod, unsigned int *data) const { + MPI_Bcast(data, 1, cm_mod::mpuint, cm_mod.master, com()); +} + /// @brief gather int array void cmType::gather(const CmMod& cm_mod, const int* send_data, int send_count, int* recv_data, int recv_count, int root) const { diff --git a/Code/Source/solver/CmMod.h b/Code/Source/solver/CmMod.h index 36ac60e18..5d2587b67 100644 --- a/Code/Source/solver/CmMod.h +++ b/Code/Source/solver/CmMod.h @@ -24,6 +24,7 @@ using MpiCommWorldType = MPI_Comm; const decltype(MPI_CXX_BOOL) mplog = MPI_CXX_BOOL; //const decltype(MPI_LOGICAL) mplog = MPI_LOGICAL; const decltype(MPI_INTEGER) mpint = MPI_INTEGER; +const decltype(MPI_UNSIGNED) mpuint = MPI_UNSIGNED; const decltype(MPI_DOUBLE_PRECISION) mpreal = MPI_DOUBLE_PRECISION; const decltype(MPI_CHARACTER) mpchar = MPI_CHARACTER; }; @@ -83,6 +84,8 @@ class cmType { void bcast(const CmMod& cm_mod, int* data) const; void bcast(const CmMod& cm_mod, Vector& data) const; + void bcast(const CmMod &cm_mod, unsigned int *data) const; + void bcast(const CmMod& cm_mod, Array& data, const std::string& name="") const; // Gather operations diff --git a/Code/Source/solver/ComMod.h b/Code/Source/solver/ComMod.h index 86db290f4..1d00fd230 100644 --- a/Code/Source/solver/ComMod.h +++ b/Code/Source/solver/ComMod.h @@ -8,19 +8,20 @@ // are defined here. #ifndef COMMOD_H -#define COMMOD_H +#define COMMOD_H #include "Array.h" #include "Array3.h" -#include "SolutionStates.h" #include "CepMod.h" #include "ChnlMod.h" #include "CmMod.h" +#include "CoupledBoundaryCondition.h" #include "Parameters.h" #include "RobinBoundaryCondition.h" -#include "CoupledBoundaryCondition.h" +#include "SolutionStates.h" #include "Timer.h" #include "Vector.h" +#include "fourier_interpolation.h" #include "DebugMsg.h" @@ -43,42 +44,6 @@ class LinearAlgebra; -/// @brief Fourier coefficients that are used to specify unsteady BCs -// -class fcType -{ - public: - - bool defined() { return n != 0; }; - - // If this is a ramp function - bool lrmp = false; - - // Number of Fourier coefficient - int n = 0; - - // No. of dimensions (scalar or vector) - int d = 0; - - // Initial value - Vector qi; - - // Time derivative of linear part - Vector qs; - - // Period - double T = 0.0; - - // Initial time - double ti = 0.0; - - // Imaginary part of coefficint - Array i; - - // Real part of coefficint - Array r; -}; - /// @brief Moving boundary data structure (used for general BC) // class MBType @@ -194,7 +159,7 @@ class bcType // // This is declare ALLOCATABLE in MOD.f. // - fcType gt; + FourierInterpolation gt; // Neu: RCR rcrType RCR; @@ -306,7 +271,7 @@ class bfType Array bx; // Time dependant (unsteady imposed value) - fcType bt; + FourierInterpolation bt; // General (unsteady and spatially dependent combination) MBType bm; @@ -334,7 +299,7 @@ class fibStrsType double eta_n = 0.0; // Unsteady time-dependent values - fcType gt; + FourierInterpolation gt; }; /// @brief Structural domain type diff --git a/Code/Source/solver/Core/Exception.h b/Code/Source/solver/Core/Exception.h index 80e6968ec..c60e92d9d 100644 --- a/Code/Source/solver/Core/Exception.h +++ b/Code/Source/solver/Core/Exception.h @@ -346,6 +346,23 @@ class DependencyException : public CoreException { } }; +class FileNotFoundException : public CoreException { +public: + FileNotFoundException(const std::string &file_name, const char *file = "", + int line = 0, const char *function = "") + : CoreException("Could not open file " + file_name, StatusCode::IOError, + file, line, function) {} +}; + +class FileFormatException : public CoreException { +public: + FileFormatException(const std::string &file_name, const std::string &message, + const char *file = "", int line = 0, + const char *function = "") + : CoreException("Error parsing file " + file_name + ". " + message, + StatusCode::IOError, file, line, function) {} +}; + inline void ExceptionRuntime::install_terminate_handler() { std::set_terminate([]() { diff --git a/Code/Source/solver/bf.cpp b/Code/Source/solver/bf.cpp index 1574b5df0..2832db0ee 100644 --- a/Code/Source/solver/bf.cpp +++ b/Code/Source/solver/bf.cpp @@ -151,8 +151,7 @@ void set_bf_l(ComMod& com_mod, bfType& lBf, mshType& lM, const SolutionStates& s f = lBf.b; } else if (utils::btest(lBf.bType, enum_int(BodyForceType::bfType_ustd))) { - Vector rtmp(1); - ifft(com_mod, lBf.bt, f, rtmp); + f = lBf.bt.value(com_mod.time); } else if (utils::btest(lBf.bType, enum_int(BodyForceType::bfType_gen))) { bfl.resize(idof,nNo); diff --git a/Code/Source/solver/distribute.cpp b/Code/Source/solver/distribute.cpp index 64acce552..742f01d31 100644 --- a/Code/Source/solver/distribute.cpp +++ b/Code/Source/solver/distribute.cpp @@ -644,44 +644,10 @@ void dist_bc(ComMod& com_mod, const CmMod& cm_mod, const cmType& cm, bcType& lBc } // Communicating time-dependent BC data - // - // lBc.gt is declare ALLOCATABLE in MOD.f but we don't - // want to use pointers so use the define() method - // to check if it has data define. - // - bool flag = lBc.gt.defined(); - cm.bcast(cm_mod, &flag); - - if (flag) { - if (is_slave) { - // [NOTE] This is allocated in ComMod. - //lBc.gt = new fcType; - } - - cm.bcast(cm_mod, &lBc.gt.lrmp); - cm.bcast(cm_mod, &lBc.gt.d); - cm.bcast(cm_mod, &lBc.gt.n); - - int j = lBc.gt.d; - int i = lBc.gt.n; - - if (is_slave) { - lBc.gt.qi.resize(j); - lBc.gt.qs.resize(j); - lBc.gt.r.resize(j,i); - lBc.gt.i.resize(j,i); - } - - cm.bcast(cm_mod, &lBc.gt.ti); - cm.bcast(cm_mod, &lBc.gt.T); - cm.bcast(cm_mod, lBc.gt.qi); - cm.bcast(cm_mod, lBc.gt.qs); - cm.bcast(cm_mod, lBc.gt.r); - cm.bcast(cm_mod, lBc.gt.i); - } + lBc.gt.distribute(cm_mod, cm); // Communicating moving BC data - flag = lBc.gm.defined(); + bool flag = lBc.gm.defined(); cm.bcast(cm_mod, &flag); if (flag) { @@ -917,31 +883,7 @@ void dist_bf(ComMod& com_mod, const CmMod& cm_mod, const cmType& cm, bfType& lBf } // Communicating time-dependent BF data - flag = lBf.bt.defined(); - cm.bcast(cm_mod, &flag); - if (flag) { - if (is_slave) { - //ALLOCATE(lBf.bt) - } - cm.bcast(cm_mod, &lBf.bt.lrmp); - cm.bcast(cm_mod, &lBf.bt.d); - cm.bcast(cm_mod, &lBf.bt.n); - - if (is_slave) { - int j = lBf.bt.d; - int i = lBf.bt.n; - lBf.bt.qi.resize(j); - lBf.bt.qs.resize(j); - lBf.bt.r.resize(j,i); - lBf.bt.i.resize(j,i); - } - cm.bcast(cm_mod, &lBf.bt.ti); - cm.bcast(cm_mod, &lBf.bt.T); - cm.bcast(cm_mod, lBf.bt.qi); - cm.bcast(cm_mod, lBf.bt.qs); - cm.bcast(cm_mod, lBf.bt.r); - cm.bcast(cm_mod, lBf.bt.i); - } + lBf.bt.distribute(cm_mod, cm); // Communicating moving BF data // @@ -1731,26 +1673,7 @@ void dist_mat_consts(const ComMod& com_mod, const CmMod& cm_mod, const cmType& c cm.bcast(cm_mod, &lStM.Tf.g); } else if (utils::btest(lStM.Tf.fType, static_cast(BoundaryConditionType::bType_ustd))) { - cm.bcast(cm_mod, &lStM.Tf.gt.lrmp); - cm.bcast(cm_mod, &lStM.Tf.gt.d); - cm.bcast(cm_mod, &lStM.Tf.gt.n); - - if (cm.slv(cm_mod)) { - int j = lStM.Tf.gt.d; - int i = lStM.Tf.gt.n; - lStM.Tf.gt.qi.resize(j); - lStM.Tf.gt.qs.resize(j); - lStM.Tf.gt.r.resize(j,i); - lStM.Tf.gt.i.resize(j,i); - } - - cm.bcast(cm_mod, &lStM.Tf.gt.ti); - cm.bcast(cm_mod, &lStM.Tf.gt.T); - - cm.bcast(cm_mod, lStM.Tf.gt.qi, "lStM.Tf.gt.qi"); - cm.bcast(cm_mod, lStM.Tf.gt.qs, "lStM.Tf.gt.qs"); - cm.bcast(cm_mod, lStM.Tf.gt.r, "lStM.Tf.gt.r"); - cm.bcast(cm_mod, lStM.Tf.gt.i, "lStM.Tf.gt.i"); + lStM.Tf.gt.distribute(cm_mod, cm); } // Broadcast directional stress distribution parameters diff --git a/Code/Source/solver/fft.cpp b/Code/Source/solver/fft.cpp index 5a7c79bf9..5a362ebd0 100644 --- a/Code/Source/solver/fft.cpp +++ b/Code/Source/solver/fft.cpp @@ -6,159 +6,6 @@ #include "fft.h" #include -/// @brief Replicates Fortran 'SUBROUTINE FFT(fid, np, gt)'. -/// -/// temporal_values contains all of the values read in from -/// the temporal values file. -// -void fft(const int np, const std::vector>& temporal_values, fcType& gt) -{ - using namespace consts; - - #define n_debug_fft - #ifdef debug_fft - DebugMsg dmsg(__func__, 0); - dmsg.banner(); - dmsg << "np: " << np; - #endif - - Vector t(np); - Array q(gt.d, np); - - for (int i = 0; i < np; i++) { - t[i] = temporal_values[i][0]; - for (int j = 0; j < gt.d; j++) { - q(j,i) = temporal_values[i][j+1]; - } - } - - - gt.ti = t[0]; - gt.T = t[np-1] - t[0]; - #ifdef debug_fft - dmsg << "pi: " << pi; - dmsg << "gt.ti: " << gt.ti; - dmsg << "gt.T: " << gt.T; - dmsg << "gt.d: " << gt.d; - dmsg << "gt.n: " << gt.n; - dmsg << "np: " << np; - dmsg << "gt.r.nrows(): " << gt.r.nrows(); - #endif - - for (int j = 0; j < gt.d; j++) { - gt.qi[j] = q(j,0); - gt.qs[j] = (q(j,np-1) - q(j,0)) / gt.T; - } - - for (int i = 0; i < np; i++) { - t[i] = t[i] - gt.ti; - for (int j = 0; j < gt.d; j++) { - q(j,i) = q(j,i) - gt.qi[j] - gt.qs[j]*t[i]; - } - } - - int ns = 0; - Vector ns_array(512); - - for (int n = 0; n < gt.n; n++) { - double tmp = static_cast(n); - gt.r.set_col(n, 0.0); - gt.i.set_col(n, 0.0); - - for (int i = 0; i < np-1; i++) { - double ko = 2.0 * pi * tmp * t[i] / gt.T; - double kn = 2.0 * pi * tmp * t[i+1] / gt.T; - - for (int j = 0; j < gt.d; j++) { - double s = (q(j,i+1) - q(j,i)) / (t[i+1] - t[i]); - if (n == 0) { - gt.r(j,n) = gt.r(j,n) + 0.5*(t[i+1] - t[i]) * (q(j,i+1) + q(j,i)); - } else { - gt.r(j,n) = gt.r(j,n) + s*(cos(kn) - cos(ko)); - gt.i(j,n) = gt.i(j,n) - s*(sin(kn) - sin(ko)); - } - } - } - - if (n == 0) { - for (int k = 0; k < gt.d; k++) { - gt.r(k,n) = gt.r(k,n) / gt.T; - } - } else { - for (int k = 0; k < gt.d; k++) { - gt.r(k,n) = 0.5 * gt.r(k,n) * gt.T / (pi*pi*tmp*tmp); - gt.i(k,n) = 0.5 * gt.i(k,n) * gt.T / (pi*pi*tmp*tmp); - } - } - } -} - -/// @brief This is to calculate flow rate and flow acceleration (IFFT) -// -void ifft(const ComMod& com_mod, const fcType& gt, Vector& Y, Vector& dY) -{ - using namespace consts; - #define n_debug_ifft - #ifdef debug_ifft - DebugMsg dmsg(__func__, com_mod.cm.idcm()); - dmsg.banner(); - #endif - - double time = com_mod.time; - #ifdef debug_ifft - dmsg << "time: " << time; - dmsg << "gt.lrmp: " << gt.lrmp; - dmsg << "gt.ti: " << gt.ti; - dmsg << "gt.n: " << gt.n; - dmsg << "pi: " << pi; - #endif - - if (gt.lrmp) { - double t = time - gt.ti; - if (t <= 0.0) { - t = fmax(t, 0.0); - } else { - t = fmin(t, gt.T); - } - #ifdef debug_ifft - dmsg << "t: " << t; - #endif - - for (int i = 0; i < gt.d; i++) { - #ifdef debug_ifft - dmsg << " gt.qi(i): " << gt.qi(i); - dmsg << " gt.qs(i): " << gt.qs(i); - #endif - Y(i) = gt.qi(i) + t*gt.qs(i); - dY(i) = gt.qs(i); - } - - } else { - double t = fmod(time - gt.ti, gt.T); - double tmp = 2.0 * pi / gt.T; - #ifdef debug_ifft - dmsg << "t: " << t; - dmsg << "tmp: " << tmp; - dmsg << "gt.d: " << gt.d; - #endif - - for (int j = 0; j < gt.d; j++) { - Y(j) = gt.qi(j) + t*gt.qs(j); - dY(j) = gt.qs(j); - } - - for (int i = 0; i < gt.n; i++) { - double dk = tmp * static_cast(i); - double K = t*dk; - - for (int j = 0; j < gt.d; j++) { - Y(j) = Y(j) + gt.r(j,i)*cos(K) - gt.i(j,i)*sin(K); - dY(j) = dY(j) - (gt.r(j,i)*sin(K) + gt.i(j,i)*cos(K))*dk; - } - } - } -} - /// @brief This routine is for calculating values by the inverse of general BC // void igbc(const ComMod& com_mod, const MBType& gm, Array& Y, Array& dY) @@ -183,7 +30,5 @@ void igbc(const ComMod& com_mod, const MBType& gm, Array& Y, Array -void fft(const int np, const std::vector>& temporal_values, fcType& gt); - -void ifft(const ComMod& com_mod, const fcType& gt, Vector& Y, Vector& dY); - void igbc(const ComMod& com_mod, const MBType& gm, Array& Y, Array& dY); #endif diff --git a/Code/Source/solver/fourier_interpolation.cpp b/Code/Source/solver/fourier_interpolation.cpp new file mode 100644 index 000000000..12aaefaff --- /dev/null +++ b/Code/Source/solver/fourier_interpolation.cpp @@ -0,0 +1,622 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the +// University of California, and others. SPDX-License-Identifier: BSD-3-Clause + +#include "fourier_interpolation.h" +#include "consts.h" + +#include +#include +#include +#include +#include + +namespace { +/// Clean a line by removing carriage returns and leading/trailing spaces, and +/// replacing multiple spaces with a single space. +std::string clean_line(std::string line) { + line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); + return std::regex_replace(line, std::regex("^ +| +$|( ) +"), "$1"); +} +} // namespace + +FourierInterpolation FourierInterpolation::from_time_series( + const unsigned int n_fourier_coefficients, + const std::vector> &temporal_values, + const bool use_ramp) { + const unsigned int n_time_points = temporal_values.size(); + + if (n_time_points < 2) { + svmp::raise( + SVMP_HERE, "At least two time points are needed to construct " + "a FourierInterpolation object."); + } + + const unsigned int n_components = temporal_values[0].size() - 1; + + // Check that all entries of temporal_values have the same number of elements. + // @todo[michelebucelli] This should probably be an Array, in which this is + // enforced a priori, rather than a vector of vectors. This change would + // also save us the extraction for loop below. + for (const auto &row : temporal_values) { + if (row.size() != n_components + 1) { + svmp::raise( + SVMP_HERE, "All rows of temporal_values must have the same number of " + "elements."); + } + } + + Vector times(n_time_points); + Array values(n_components, n_time_points); + + for (unsigned int i = 0; i < n_time_points; ++i) { + times[i] = temporal_values[i][0]; + for (unsigned int j = 0; j < n_components; ++j) { + values(j, i) = temporal_values[i][j + 1]; + } + } + + FourierInterpolation result; + + result.use_ramp = use_ramp; + result.n_components = n_components; + result.n_fourier_coefficients = use_ramp ? 1 : n_fourier_coefficients; + result.initial_time = times[0]; + result.period = times[n_time_points - 1] - times[0]; + result.linear_trend_initial_values.resize(n_components); + result.linear_trend_slopes.resize(n_components); + result.fourier_coefficients_real.resize(n_components, + result.n_fourier_coefficients); + result.fourier_coefficients_imaginary.resize(n_components, + result.n_fourier_coefficients); + + // Compute the linear trend part. + for (unsigned int j = 0; j < n_components; ++j) { + result.linear_trend_initial_values[j] = values(j, 0); + result.linear_trend_slopes[j] = + (values(j, n_time_points - 1) - values(j, 0)) / result.period; + } + + // Subtract the linear trend part from the values. + for (unsigned int i = 0; i < n_time_points; ++i) { + times[i] -= result.initial_time; + for (unsigned int j = 0; j < n_components; ++j) { + values(j, i) = values(j, i) - result.linear_trend_initial_values[j] - + result.linear_trend_slopes[j] * times[i]; + } + } + + // Compute the Fourier coefficients. + for (unsigned int n = 0; n < result.n_fourier_coefficients; ++n) { + const double tmp = static_cast(n); + result.fourier_coefficients_real.set_col(n, 0.0); + result.fourier_coefficients_imaginary.set_col(n, 0.0); + + for (unsigned int i = 0; i < n_time_points - 1; ++i) { + const double ko = 2.0 * consts::pi * tmp * times[i] / result.period; + const double kn = 2.0 * consts::pi * tmp * times[i + 1] / result.period; + + for (unsigned int j = 0; j < result.n_components; j++) { + const double s = + (values(j, i + 1) - values(j, i)) / (times[i + 1] - times[i]); + + if (n == 0) { + result.fourier_coefficients_real(j, n) += + 0.5 * (times[i + 1] - times[i]) * + (values(j, i + 1) + values(j, i)); + } else { + result.fourier_coefficients_real(j, n) += + s * (std::cos(kn) - std::cos(ko)); + result.fourier_coefficients_imaginary(j, n) -= + s * (std::sin(kn) - std::sin(ko)); + } + } + } + + if (n == 0) { + for (unsigned int k = 0; k < result.n_components; k++) { + result.fourier_coefficients_real(k, n) /= result.period; + } + } else { + const double tmp_2 = (consts::pi * consts::pi * tmp * tmp); + + for (unsigned int k = 0; k < result.n_components; k++) { + result.fourier_coefficients_real(k, n) = + 0.5 * result.fourier_coefficients_real(k, n) * result.period / + tmp_2; + result.fourier_coefficients_imaginary(k, n) = + 0.5 * result.fourier_coefficients_imaginary(k, n) * result.period / + tmp_2; + } + } + } + + return result; +} + +FourierInterpolation +FourierInterpolation::from_time_series_file(const std::string &file_name, + const unsigned int n_components, + const bool use_ramp) { + std::ifstream file(file_name); + + if (!file.is_open()) { + svmp::raise(SVMP_HERE, file_name); + } + + // Read the header of the file. + int n_time_points, n_fourier_coefficients; + file >> n_time_points >> n_fourier_coefficients; + + if (n_time_points < 2) { + svmp::raise( + SVMP_HERE, file_name, + "At least 2 time points are required to set up Fourier interpolation. " + "Only " + + std::to_string(n_time_points) + " time points were provided."); + } + + if (n_fourier_coefficients == 0) { + svmp::raise( + SVMP_HERE, file_name, + "At least 1 Fourier coefficient is required to set up Fourier " + "interpolation. 0 Fourier coefficients were provided."); + } + + // Read the time-value pairs. + std::vector> values; + double tmp; + + std::string line; + int line_number = 1; + + while (std::getline(file, line)) { + line = clean_line(line); + if (line.empty()) + continue; + + std::istringstream line_string_stream(line); + std::vector line_values; + + while (!line_string_stream.eof()) { + line_string_stream >> tmp; + + if (line_string_stream.fail()) { + svmp::raise( + SVMP_HERE, file_name, + "Error reading values for the temporal values file for line " + + std::to_string(line_number) + ": '" + line + + "'; value number " + std::to_string(line_values.size() + 1) + + " is not a double."); + } + + line_values.push_back(tmp); + } + + if (line_values.size() != 1 + n_components) { + svmp::raise( + SVMP_HERE, file_name, + "Error reading values for the temporal values file for line " + + std::to_string(line_number) + ": '" + line + "'; expected " + + std::to_string(1 + n_components) + " values, but got " + + std::to_string(line_values.size()) + "."); + } + + values.push_back(line_values); + ++line_number; + } + + return FourierInterpolation::from_time_series(n_fourier_coefficients, values, + use_ramp); +} + +FourierInterpolation FourierInterpolation::from_fourier_coefficients( + const Vector &linear_trend_initial_values, + const Vector &linear_trend_slopes, + const Array &fourier_coefficients_real, + const Array &fourier_coefficients_imaginary, + const double initial_time, const double period) { + // Linear trend initial values and slopes must have the same size (which will + // determine the number of components). + if (linear_trend_initial_values.size() != linear_trend_slopes.size()) { + svmp::raise( + SVMP_HERE, + "linear_trend_initial_values and linear_trend_slopes must have the " + "same size, but their sizes are " + + std::to_string(linear_trend_initial_values.size()) + " and " + + std::to_string(linear_trend_slopes.size()) + "."); + } + + // The number of rows of the Fourier coefficients must match the number of + // components (i.e. the size of the linear trend vectors). + if (fourier_coefficients_real.nrows() != linear_trend_initial_values.size()) { + svmp::raise( + SVMP_HERE, "The number of rows of fourier_coefficients_real must match " + "the size of linear_trend_initial_values."); + } + + // The number of rows of the Fourier coefficients must match the number of + // components (i.e. the size of the linear trend vectors). + if (fourier_coefficients_imaginary.nrows() != + linear_trend_initial_values.size()) { + svmp::raise( + SVMP_HERE, "The number of rows of fourier_coefficients_imaginary must " + "match the size of linear_trend_initial_values."); + } + + // The number of columns of the real and imaginary Fourier coefficients must + // match. + if (fourier_coefficients_real.ncols() != + fourier_coefficients_imaginary.ncols()) { + svmp::raise( + SVMP_HERE, "The number of columns of fourier_coefficients_real and " + "fourier_coefficients_imaginary must match."); + } + + FourierInterpolation result; + + result.use_ramp = false; + result.n_fourier_coefficients = fourier_coefficients_real.ncols(); + result.n_components = linear_trend_initial_values.size(); + result.linear_trend_initial_values = linear_trend_initial_values; + result.linear_trend_slopes = linear_trend_slopes; + result.fourier_coefficients_real = fourier_coefficients_real; + result.fourier_coefficients_imaginary = fourier_coefficients_imaginary; + result.initial_time = initial_time; + result.period = period; + + return result; +} + +FourierInterpolation FourierInterpolation::from_fourier_coefficients_file( + const std::string &file_name, const unsigned int n_components) { + std::ifstream file(file_name); + + if (!file.is_open()) { + svmp::raise(SVMP_HERE, file_name); + } + + double initial_time, period; + file >> initial_time >> period; + + if (file.fail()) { + svmp::raise( + SVMP_HERE, file_name, + "Could not read the initial time and period from the first line."); + } + + // Read the linear trend part. Each line is expected to contain the initial + // value and slope of the linear trend for one component, so it must hold at + // least 2 values. + Vector linear_trend_initial_values(n_components); + Vector linear_trend_slopes(n_components); + + std::string line; + double tmp; + unsigned int current_component = 0; + + while (current_component < n_components && std::getline(file, line)) { + line = clean_line(line); + if (line.empty()) + continue; + + std::istringstream line_string_stream(line); + std::vector values; + + while (line_string_stream >> tmp) { + values.push_back(tmp); + } + + if (values.size() != 2) { + svmp::raise( + SVMP_HERE, file_name, + "Error reading the linear trend for component " + + std::to_string(current_component) + ": '" + line + + "'; expected exactly 2 values (initial value and slope), but " + "got " + + std::to_string(values.size()) + "."); + } + + linear_trend_initial_values[current_component] = values[0]; + linear_trend_slopes[current_component] = values[1]; + ++current_component; + } + + if (current_component != n_components) { + svmp::raise( + SVMP_HERE, file_name, + "Expected " + std::to_string(n_components) + + " lines of linear trend coefficients (one per component), but only " + "found " + + std::to_string(current_component) + "."); + } + + // Read the Fourier coefficients. + unsigned int n_fourier_coefficients; + file >> n_fourier_coefficients; + + if (file.fail()) { + svmp::raise( + SVMP_HERE, file_name, + "Could not read the number of Fourier coefficients."); + } + + Array fourier_coefficients_real(n_components, n_fourier_coefficients); + Array fourier_coefficients_imaginary(n_components, + n_fourier_coefficients); + + // Each line is expected to hold the real parts of the coefficients for all + // components followed by the imaginary parts, so it must hold exactly + // 2 * n_components values. + const unsigned int n_values_per_line = 2 * n_components; + + unsigned int current_coefficient = 0; + while (current_coefficient < n_fourier_coefficients && + std::getline(file, line)) { + line = clean_line(line); + if (line.empty()) + continue; + + std::istringstream line_string_stream(line); + std::vector values; + + while (line_string_stream >> tmp) { + values.push_back(tmp); + } + + if (values.size() != n_values_per_line) { + svmp::raise( + SVMP_HERE, file_name, + "Error reading Fourier coefficient " + + std::to_string(current_coefficient) + ": '" + line + + "'; expected " + std::to_string(n_values_per_line) + + " values (real and imaginary parts for " + + std::to_string(n_components) + " components), but got " + + std::to_string(values.size()) + "."); + } + + for (unsigned int j = 0; j < n_components; ++j) { + fourier_coefficients_real(j, current_coefficient) = values[j]; + fourier_coefficients_imaginary(j, current_coefficient) = + values[j + n_components]; + } + + ++current_coefficient; + } + + if (current_coefficient != n_fourier_coefficients) { + svmp::raise( + SVMP_HERE, file_name, + "Expected " + std::to_string(n_fourier_coefficients) + + " lines of Fourier coefficients, but only found " + + std::to_string(current_coefficient) + "."); + } + + return FourierInterpolation::from_fourier_coefficients( + linear_trend_initial_values, linear_trend_slopes, + fourier_coefficients_real, fourier_coefficients_imaginary, initial_time, + period); +} + +void FourierInterpolation::distribute(const CmMod &cm_mod, const cmType &cm) { + // Only the master knows whether the object has been initialized. Therefore, + // we broadcast the initialization flag to all ranks, so that the following if + // statement can be run correctly by all. + bool initialized = defined(); + cm.bcast(cm_mod, &initialized); + + if (initialized) { + cm.bcast(cm_mod, &use_ramp); + cm.bcast(cm_mod, &n_fourier_coefficients); + cm.bcast(cm_mod, &n_components); + + // All ranks but the master need to allocate the arrays before receiving + // data. + if (cm.slv(cm_mod)) { + linear_trend_initial_values.resize(n_components); + linear_trend_slopes.resize(n_components); + fourier_coefficients_real.resize(n_components, n_fourier_coefficients); + fourier_coefficients_imaginary.resize(n_components, + n_fourier_coefficients); + } + + cm.bcast(cm_mod, &initial_time); + cm.bcast(cm_mod, &period); + cm.bcast(cm_mod, linear_trend_initial_values); + cm.bcast(cm_mod, linear_trend_slopes); + cm.bcast(cm_mod, fourier_coefficients_real); + cm.bcast(cm_mod, fourier_coefficients_imaginary); + } +} + +Vector FourierInterpolation::value(const double time) const { + Vector result(n_components); + static Vector dummy; + + evaluate_internal(time, /* evaluate_derivative = */ false, result, dummy); + + return result; +} + +std::pair, Vector> +FourierInterpolation::value_and_derivative(const double time) const { + Vector value(n_components); + Vector derivative(n_components); + + evaluate_internal(time, /* evaluate_derivative = */ true, value, derivative); + + return std::make_pair(value, derivative); +} + +bool FourierInterpolation::defined() const { + return n_fourier_coefficients != 0; +} + +unsigned int FourierInterpolation::get_n_components() const { + return n_components; +} + +unsigned int FourierInterpolation::get_n_fourier_coefficients() const { + return n_fourier_coefficients; +} + +double FourierInterpolation::get_linear_trend_initial_value( + const unsigned int component) const { + if (!defined()) { + svmp::raise( + SVMP_HERE, + "Cannot get linear trend initial value of FourierInterpolation " + "instance that has not been defined."); + } + + if (component >= n_components) { + svmp::raise( + SVMP_HERE, "Component index " + std::to_string(component) + + " is out of bounds for FourierInterpolation instance " + "with " + + std::to_string(n_components) + " components."); + } + + return linear_trend_initial_values[component]; +} + +double FourierInterpolation::get_linear_trend_slope( + const unsigned int component) const { + if (!defined()) { + svmp::raise( + SVMP_HERE, + "Cannot get linear trend initial value of FourierInterpolation " + "instance that has not been defined."); + } + + if (component >= n_components) { + svmp::raise( + SVMP_HERE, "Component index " + std::to_string(component) + + " is out of bounds for FourierInterpolation instance " + "with " + + std::to_string(n_components) + " components."); + } + + return linear_trend_slopes[component]; +} + +double +FourierInterpolation::get_coefficient_real(const unsigned int component, + const unsigned int frequency) const { + if (!defined()) { + svmp::raise( + SVMP_HERE, + "Cannot get linear trend initial value of FourierInterpolation " + "instance that has not been defined."); + } + + if (component >= n_components) { + svmp::raise( + SVMP_HERE, "Component index " + std::to_string(component) + + " is out of bounds for FourierInterpolation instance " + "with " + + std::to_string(n_components) + " components."); + } + + if (frequency >= n_fourier_coefficients) { + svmp::raise( + SVMP_HERE, "Frequency index " + std::to_string(frequency) + + " is out of bounds for FourierInterpolation instance " + "with " + + std::to_string(n_fourier_coefficients) + + " Fourier coefficients."); + } + + return fourier_coefficients_real(component, frequency); +} + +double FourierInterpolation::get_coefficient_imaginary( + const unsigned int component, const unsigned int frequency) const { + + if (!defined()) { + svmp::raise( + SVMP_HERE, + "Cannot get linear trend initial value of FourierInterpolation " + "instance that has not been defined."); + } + + if (component >= n_components) { + svmp::raise( + SVMP_HERE, "Component index " + std::to_string(component) + + " is out of bounds for FourierInterpolation instance " + "with " + + std::to_string(n_components) + " components."); + } + + if (frequency >= n_fourier_coefficients) { + svmp::raise( + SVMP_HERE, "Frequency index " + std::to_string(frequency) + + " is out of bounds for FourierInterpolation instance " + "with " + + std::to_string(n_fourier_coefficients) + + " Fourier coefficients."); + } + + return fourier_coefficients_imaginary(component, frequency); +} + +void FourierInterpolation::evaluate_internal(const double time, + const bool evaluate_derivative, + Vector &value, + Vector &derivative) const { + if (!defined()) { + svmp::raise( + SVMP_HERE, "Cannot evaluate FourierInterpolation instance that has not " + "been defined."); + } + + // Shifted and rescaled time. + // The input time is shifted by ti. Then, if using the ramp function, it is + // clamped to the interval [0, T]. Otherwise, the time is wrapped to the + // interval [0, T], to enable periodicity. + const double t = use_ramp + ? std::max(std::min(time - initial_time, period), 0.0) + : std::fmod(time - initial_time, period); + + // Linear trend. + for (unsigned int i = 0; i < n_components; ++i) { + value[i] = linear_trend_initial_values[i] + t * linear_trend_slopes[i]; + + // @todo[michelebucelli] This is consistent with the old code, but is + // incorrect when use_ramp = true. In that case, the derivative should be + // zero outside the interpolation interval [initial_time, initial_time + + // period]. + if (evaluate_derivative) + derivative[i] = linear_trend_slopes[i]; + } + + // Fourier series. + if (!use_ramp) { + const double tmp = 2.0 * consts::pi / period; + + // Fourier series. + for (int i = 0; i < n_fourier_coefficients; ++i) { + const double dk = tmp * i; + const double K = t * dk; + + for (int j = 0; j < n_components; ++j) { + // Using value[j] = value[j] + ... instead of value[j] += ..., because + // the latter changes the order of operations enough to break some of + // the tests. + // @todo[michelebucelli] This seems pretty fragile! It happens in a few + // other places in this file as well, where using an increment + // operator (*= or +=) changes the order of operations resulting in + // changes in the test values. This should be investigated and the + // best (i.e. most accurate) operation order should be chosen. + value[j] = value[j] + fourier_coefficients_real(j, i) * std::cos(K) - + fourier_coefficients_imaginary(j, i) * std::sin(K); + + if (evaluate_derivative) { + derivative[j] -= + (fourier_coefficients_real(j, i) * std::sin(K) + + fourier_coefficients_imaginary(j, i) * std::cos(K)) * + dk; + } + } + } + } +} \ No newline at end of file diff --git a/Code/Source/solver/fourier_interpolation.h b/Code/Source/solver/fourier_interpolation.h new file mode 100644 index 000000000..cf898cf66 --- /dev/null +++ b/Code/Source/solver/fourier_interpolation.h @@ -0,0 +1,397 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the +// University of California, and others. SPDX-License-Identifier: BSD-3-Clause + +#ifndef FOURIER_INTERPOLATION_H +#define FOURIER_INTERPOLATION_H + +#include "Array.h" +#include "CmMod.h" +#include "Vector.h" + +#include "Core/Exception.h" +#include "FE/Common/FEException.h" + +#include +#include +#include + +/** + * @brief Fourier interpolation of time dependent data. + * + * This class implements interpolation of time dependent data through either a + * Fourier series or a linear clamped ramp. Which of the two is used is + * determined by the boolean member variable @ref use_ramp. + * + * In what follows, let @f$\{t_i, \mathbf{v}_i\}_{i=0}^{N-1}@f$ be the + * interpolated time series, and @f$T = t_{N-1} - t_0@f$ be the period of the + * time series. The time series is assumed to be ordered, i.e. @f$t_i < + * t_{i+1}@f$ for all @f$i@f$. + * + * ## Using this class + * + * The FourierInterpolation class is not meant to be constructed directly, but + * rather through one of the static methods @ref from_time_series, @ref + * from_time_series_file, @ref from_fourier_coefficients, or @ref + * from_fourier_coefficients_file. + * + * If data is only read from the master rank in a parallel setting, the method + * @ref distribute can be used to broadcast the data to all ranks. + * + * Once the object has been constructed, the interpolated value at a given time + * can be obtained through the method @ref value, and the interpolated value and + * its time derivative can be obtained through the method @ref + * value_and_derivative. + * + * ## Fourier series interpolation + * + * This gives rise to a periodic interpolation, with period @f$T@f$. Let @f$M@f$ + * be the user-defined number of Fourier modes. The interpolated value at time + * @f$t@f$ is given by + * @f[ \begin{aligned} + * \tilde{\mathbf{v}}(t) &= + * \underbrace{\mathbf{q}_i + \mathbf{q}_s \tau(t)}_{\text{linear trend}} + + * \underbrace{ + * \sum_{k=0}^{M-1} \left( + * \mathbf{c}_k^{\text{real}} \cos\left(\frac{2 \pi k \tau(t)}{T}\right) - + * \mathbf{c}_k^{\text{imag}} \sin\left(\frac{2 \pi k \tau(t)}{T}\right) + * \right) + * }_{\text{Fourier series}} \\ + * \tau(t) &= (t - t_0)\;\text{mod}\;T + * \end{aligned} @f] + * The coefficients of the linear trend and Fourier series are determined from + * the input time series as follows: + * @f[ \begin{aligned} + * \mathbf{q}_i &= \mathbf{v}_0 \\ + * \mathbf{q}_s &= \frac{\mathbf{v}_{N-1} - \mathbf{v}_0}{T} \\ + * \mathbf{c}_0^{\text{real}} &= + * \frac{1}{2T} \sum_{i=0}^{N-2} (\hat{t}_{i+1} - \hat{t}_i) + * (\hat{\mathbf{v}}_{i+1} + \hat{\mathbf{v}}_i) \\ + * \mathbf{c}_0^{\text{imag}} &= 0 \\ + * \mathbf{c}_k^{\text{real}} &= \frac{T}{2 \pi^2 k^2} + * \sum_{i=0}^{N-2} \frac{\hat{\mathbf{v}}_{i+1} - \hat{\mathbf{v}}_i} + * {\hat{t}_{i+1} - \hat{t}_i} \left( + * \cos\left(\frac{2 \pi k \hat{t}_{i+1}}{T}\right) - + * \cos\left(\frac{2 \pi k \hat{t}_i}{T}\right) + * \right) \\ + * \mathbf{c}_k^{\text{imag}} &= \frac{T}{2 \pi^2 k^2} + * -\sum_{i=0}^{N-2} \frac{\hat{\mathbf{v}}_{i+1} - \hat{\mathbf{v}}_i} + * {\hat{t}_{i+1} - \hat{t}_i} \left( + * \sin\left(\frac{2 \pi k \hat{t}_{i+1}}{T}\right) - + * \sin\left(\frac{2 \pi k \hat{t}_i}{T}\right) + * \right) + * \end{aligned} @f] + * The quantities @f$\hat{t}_i@f$ and @f$\hat{\mathbf{v}}_i@f$ are the time and + * value series after subtracting the linear trend: + * @f[ \begin{aligned} + * \hat{t}_i &= t_i - t_0 \\ + * \hat{\mathbf{v}}_i &= \mathbf{v}_i - \mathbf{q}_i - \mathbf{q}_s \hat{t}_i + * \end{aligned} @f] + * + * ## Ramp interpolation + * + * The interpolated value is equal to @f$\mathbf{v}_0@f$ until time @f$t_0@f$, + * then it follows a linear ramp from @f$\mathbf{v}_0@f$ to + * @f$\mathbf{v}_{N-1}@f$ at @f$t_{N-1}@f$, and then it remains constant at + * @f$\mathbf{v}_{N-1}@f$ for all times greater than @f$t_{N-1}@f$. Notice in + * particular that this interpolation is not periodic. + * + * The interpolated value is given by + * @f[ + * \tilde{\mathbf{v}}(t) = \begin{cases} + * \mathbf{v}_0 & t < t_0 \\ + * \mathbf{v}_0 + \frac{\mathbf{v}_{N-1} + * - \mathbf{v}_0}{t_{N-1} - t_0} (t - t_0) + * & t_0 \leq t < t_{N-1} \\ + * \mathbf{v}_{N-1} & t \geq t_{N-1} + * \end{cases} + * @f] + * + * This is equivalent to only taking the linear trend part of the Fourier series + * interpolation, and clamping the time to the interval @f$[t_0, t_{N-1}]@f$. + * + */ +class FourierInterpolation { +public: + /** + * @brief Default constructor. + * + * This constructor is not meant to be used directly, and is only here to + * facilitate storing objects of this type in STL containers. The object + * constructed this way is not initialized and will not be usable until it is + * assigned to a valid FourierInterpolation instance. Use the static methods + * @ref from_time_series, @ref from_time_series_file, @ref + * from_fourier_coefficients, or @ref from_fourier_coefficients_file to + * construct a valid instance. + */ + FourierInterpolation() = default; + + /** + * @brief Construct a FourierInterpolation from a time series. + * + * @param[in] n_fourier_coefficients The number @f$M@f$ of Fourier modes to + * use in the interpolation. + * @param[in] temporal_values The time series to interpolate. This is a vector + * of vectors, where each inner vector contains the time and the values of + * all components at that time. In other words, + * temporal_values[i][0] is the time @f$t_i@f$, and + * temporal_values[i][j] is the value the @f$j@f$-th component of + * @f$\mathbf{v}_i@f$. + * @param[in] use_ramp Whether to use a ramp function for the interpolation. + * See the general class documentation for the precise meaning of this + * choice. + */ + static FourierInterpolation + from_time_series(const unsigned int n_fourier_coefficients, + const std::vector> &temporal_values, + const bool use_ramp); + + /** + * @brief Read a time series from file and return the corresponding instance + * of FourierInterpolation. + * + * The input file is expected to have the following format: + * ``` + * + *