/*---------------------------------------------------------------------------*\ ========= | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox \\ / O peration | \\ / A nd | www.openfoam.com \\/ M anipulation | ------------------------------------------------------------------------------- Copyright (C) 2020-2022 OpenCFD Ltd. ------------------------------------------------------------------------------- License This file is part of OpenFOAM. OpenFOAM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OpenFOAM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenFOAM. If not, see . \*---------------------------------------------------------------------------*/ #include "STDMD.H" #include "EigenMatrix.H" #include "QRMatrix.H" #include "addToRunTimeSelectionTable.H" using namespace Foam::constant::mathematical; // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * // namespace Foam { namespace DMDModels { defineTypeNameAndDebug(STDMD, 0); addToRunTimeSelectionTable(DMDModel, STDMD, dictionary); } } const Foam::Enum < Foam::DMDModels::STDMD::modeSorterType > Foam::DMDModels::STDMD::modeSorterTypeNames ({ { modeSorterType::FIRST_SNAPSHOT, "firstSnapshot" }, { modeSorterType::KIEWAT , "kiewat" }, { modeSorterType::KOU_ZHANG , "kouZhang" } }); // * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * // Foam::scalar Foam::DMDModels::STDMD::L2norm(const RMatrix& z) const { #ifdef FULLDEBUG // Check if the given RectangularMatrix is effectively a column vector if (z.n() != 1) { FatalErrorInFunction << "Input matrix is not a column vector." << exit(FatalError); } #endif const bool noSqrt = true; scalar result = z.columnNorm(0, noSqrt); reduce(result, sumOp()); // Heuristic addition to avoid very small or zero norm return max(SMALL, Foam::sqrt(result)); } Foam::RectangularMatrix Foam::DMDModels::STDMD::orthonormalise ( RMatrix ez ) const { List dz(Q_.n()); const label sz0 = ez.m(); const label sz1 = dz.size(); for (label i = 0; i < nGramSchmidt_; ++i) { // dz = Q_ & ez; dz = Zero; for (label i = 0; i < sz0; ++i) { for (label j = 0; j < sz1; ++j) { dz[j] += Q_(i,j)*ez(i,0); } } reduce(dz, sumOp>()); // ez -= Q_*dz; for (label i = 0; i < sz0; ++i) { scalar t = 0; for (label j = 0; j < sz1; ++j) { t += Q_(i,j)*dz[j]; } ez(i,0) -= t; } } return ez; } void Foam::DMDModels::STDMD::expand(const RMatrix& ez, const scalar ezNorm) { Info<< tab << "Expanding orthonormal basis for field: " << fieldName_ << endl; // Stack a column "(ez/ezNorm)" to "Q" Q_.resize(Q_.m(), Q_.n() + 1); Q_.subColumn(Q_.n() - 1) = ez/ezNorm; // Stack a row (Zero) and column (Zero) to "G" G_.resize(G_.m() + 1); } void Foam::DMDModels::STDMD::updateG(const RMatrix& z) { List zTilde(Q_.n(), Zero); const label sz0 = z.m(); const label sz1 = zTilde.size(); // zTilde = Q_ & z; for (label i = 0; i < sz0; ++i) { for (label j = 0; j < sz1; ++j) { zTilde[j] += Q_(i,j)*z(i,0); } } reduce(zTilde, sumOp>()); // G_ += SMatrix(zTilde^zTilde); for (label i = 0; i < G_.m(); ++i) { for (label j = 0; j < G_.n(); ++j) { G_(i, j) += zTilde[i]*zTilde[j]; } } } void Foam::DMDModels::STDMD::compress() { Info<< tab << "Compressing orthonormal basis for field: " << fieldName_ << endl; RMatrix q(1, 1, Zero); if (Pstream::master()) { const bool symmetric = true; const EigenMatrix EM(G_, symmetric); const SquareMatrix& EVecs = EM.EVecs(); DiagonalMatrix EVals(EM.EValsRe()); // Sort eigenvalues in descending order, and track indices const auto descend = [](scalar a, scalar b){ return a > b; }; const labelList permutation(EVals.sortPermutation(descend)); EVals.applyPermutation(permutation); EVals.resize(EVals.size() - 1); // Update "G" G_ = SMatrix(maxRank_, Zero); G_.diag(EVals); q.resize(Q_.n(), maxRank_); for (label i = 0; i < maxRank_; ++i) { q.subColumn(i) = EVecs.subColumn(permutation[i]); } } Pstream::broadcast(G_); Pstream::broadcast(q); // Update "Q" Q_ = Q_*q; } Foam::SquareMatrix Foam::DMDModels::STDMD:: reducedKoopmanOperator() { Info<< tab << "Computing Atilde" << endl; Info<< tab << "Computing local Rx" << endl; RMatrix Rx(1, 1, Zero); { QRMatrix QRM ( Qupper_, QRMatrix::modes::ECONOMY, QRMatrix::outputs::ONLY_R ); RMatrix& R = QRM.R(); Rx.transfer(R); } Rx.round(); // Convenience objects A1, A2, A3 to compute "Atilde" RMatrix A1(1, 1, Zero); if (Pstream::parRun()) { // Parallel direct tall-skinny QR decomposition // (BGD:Fig. 5) & (DGHL:Fig. 2) // Tests revealed that the distribution of "Q" does not affect // the final outcome of TSQR decomposition up to sign // Don't clear storage on persistent buffer PstreamBuffers pBufs; pBufs.allowClearRecv(false); const label myProcNo = Pstream::myProcNo(); const label procNoInSubset = myProcNo % nAgglomerationProcs_; // Send Rx from sender subset neighbours to receiver subset master if (procNoInSubset != 0) { const label procNoSubsetMaster = myProcNo - procNoInSubset; UOPstream toSubsetMaster(procNoSubsetMaster, pBufs); toSubsetMaster << Rx; Rx.clear(); } pBufs.finishedSends(); // Receive Rx by receiver subset masters if (procNoInSubset == 0) { // Accept only subset masters possessing sender subset neighbours if (myProcNo + 1 < Pstream::nProcs()) { for ( label nbr = myProcNo + 1; ( nbr < myProcNo + nAgglomerationProcs_ && nbr < Pstream::nProcs() ); ++nbr ) { RMatrix recvMtrx; UIPstream fromNbr(nbr, pBufs); fromNbr >> recvMtrx; // Append received Rx to Rx of receiver subset masters if (recvMtrx.size() > 0) { Rx.resize(Rx.m() + recvMtrx.m(), Rx.n()); Rx.subMatrix ( Rx.m() - recvMtrx.m(), Rx.n() - recvMtrx.n() ) = recvMtrx; } } } { // Apply interim QR decomposition // on Rx of receiver subset masters QRMatrix QRM ( QRMatrix::modes::ECONOMY, QRMatrix::outputs::ONLY_R, QRMatrix::pivoting::FALSE, Rx ); RMatrix& R = QRM.R(); Rx.transfer(R); } Rx.round(); } pBufs.clear(); // Send interim Rx from subset masters to the master if (procNoInSubset == 0) { if (!Pstream::master()) { UOPstream toMaster(Pstream::masterNo(), pBufs); toMaster << Rx; } } pBufs.finishedSends(); // Receive interim Rx by the master, and apply final operations if (Pstream::master()) { for ( label nbr = Pstream::masterNo() + nAgglomerationProcs_; nbr < Pstream::nProcs(); nbr += nAgglomerationProcs_ ) { RMatrix recvMtrx; UIPstream fromSubsetMaster(nbr, pBufs); fromSubsetMaster >> recvMtrx; // Append received Rx to Rx of the master if (recvMtrx.size() > 0) { Rx.resize(Rx.m() + recvMtrx.m(), Rx.n()); Rx.subMatrix ( Rx.m() - recvMtrx.m(), Rx.n() - recvMtrx.n() ) = recvMtrx; } } Info<< tab << "Computing TSQR" << endl; { QRMatrix QRM ( QRMatrix::modes::ECONOMY, QRMatrix::outputs::ONLY_R, QRMatrix::pivoting::FALSE, Rx ); RMatrix& R = QRM.R(); Rx.transfer(R); } Rx.round(); Info<< tab << "Computing RxInv" << endl; RxInv_ = MatrixTools::pinv(Rx); Info<< tab << "Computing A1" << endl; A1 = RxInv_*RMatrix(MatrixTools::pinv(Rx*(G_^Rx))); Rx.clear(); } Pstream::broadcast(RxInv_); Pstream::broadcast(A1); Info<< tab << "Computing A2" << endl; SMatrix A2(Qupper_ & Qlower_); reduce(A2, sumOp()); Qlower_.clear(); Info<< tab << "Computing A3" << endl; SMatrix A3(Qupper_ & Qupper_); reduce(A3, sumOp()); Info<< tab << "Computing Atilde" << endl; // by optimized matrix chain multiplication // obtained by dynamic programming memoisation return SMatrix(RxInv_ & ((A2*(G_*A3))*A1)); } else { Info<< tab << "Computing RxInv" << endl; RxInv_ = MatrixTools::pinv(Rx); Info<< tab << "Computing A1" << endl; A1 = RxInv_*RMatrix(MatrixTools::pinv(Rx*(G_^Rx))); Rx.clear(); Info<< tab << "Computing A2" << endl; SMatrix A2(Qupper_ & Qlower_); Qlower_.clear(); Info<< tab << "Computing A3" << endl; SMatrix A3(Qupper_ & Qupper_); Info<< tab << "Computing Atilde" << endl; return SMatrix(RxInv_ & ((A2*(G_*A3))*A1)); } } bool Foam::DMDModels::STDMD::eigendecomposition(SMatrix& Atilde) { bool fail = false; // Compute eigenvalues, and clip eigenvalues with values < "minEval" if (Pstream::master()) { Info<< tab << "Computing eigendecomposition" << endl; // Replace "Atilde" by eigenvalues (in-place eigendecomposition) const EigenMatrix EM(Atilde); const DiagonalMatrix& evalsRe = EM.EValsRe(); const DiagonalMatrix& evalsIm = EM.EValsIm(); evals_.resize(evalsRe.size()); evecs_ = RCMatrix(EM.complexEVecs()); // Convert scalar eigenvalue pairs to complex eigenvalues label i = 0; for (auto& eval : evals_) { eval = complex(evalsRe[i], evalsIm[i]); ++i; } Info<< tab << "Filtering eigenvalues" << endl; List cp(evals_.size()); auto it = std::copy_if ( evals_.cbegin(), evals_.cend(), cp.begin(), [&](const complex& x){ return mag(x) > minEval_; } ); cp.resize(std::distance(cp.begin(), it)); if (cp.size() == 0) { WarningInFunction << "No eigenvalue with mag(eigenvalue) larger than " << "minEval = " << minEval_ << " was found." << nl << " Input field may contain only zero-value elements," << nl << " e.g. no-slip velocity condition on a given patch." << nl << " Otherwise, please decrease the value of minEval." << nl << " Skipping further dynamics/mode computations." << nl << endl; fail = true; } else { evals_ = cp; } } Pstream::broadcast(fail); if (fail) { return false; } Pstream::broadcast(evals_); Pstream::broadcast(evecs_); return true; } void Foam::DMDModels::STDMD::frequencies() { if (Pstream::master()) { Info<< tab << "Computing frequencies" << endl; freqs_.resize(evals_.size()); // Frequency equation (K:Eq. 81) auto frequencyEquation = [&](const complex& eval) { return Foam::log(max(eval, complex(SMALL))).imag()/(twoPi*dt_); }; // Compute frequencies std::transform ( evals_.cbegin(), evals_.cend(), freqs_.begin(), frequencyEquation ); Info<< tab << "Computing frequency indices" << endl; // Initialise iterator by the first search auto margin = [&](const scalar& x){ return (fMin_ <= x && x < fMax_); }; auto it = std::find_if(freqs_.cbegin(), freqs_.cend(), margin); while (it != freqs_.end()) { freqsi_.append(std::distance(freqs_.cbegin(), it)); it = std::find_if(std::next(it), freqs_.cend(), margin); } } Pstream::broadcast(freqs_); Pstream::broadcast(freqsi_); } void Foam::DMDModels::STDMD::amplitudes() { IOField snapshot0 ( IOobject ( "snapshot0_" + name_ + "_" + fieldName_, timeName0_, mesh_, IOobject::MUST_READ, IOobject::NO_WRITE, IOobject::NO_REGISTER ) ); // RxInv^T*(Qupper^T*snapshot0) // tmp = (Qupper^T*snapshot0) List tmp(Qupper_.n(), Zero); const label sz0 = snapshot0.size(); const label sz1 = tmp.size(); for (label i = 0; i < sz0; ++i) { for (label j = 0; j < sz1; ++j) { tmp[j] += Qupper_(i,j)*snapshot0[i]; } } snapshot0.clear(); // R = RxInv^T*tmp List R(Qupper_.n(), Zero); for (label i = 0; i < sz1; ++i) { for (label j = 0; j < R.size(); ++j) { R[j] += RxInv_(i,j)*tmp[i]; } } tmp.clear(); reduce(R, sumOp>()); if (Pstream::master()) { Info<< tab << "Computing amplitudes" << endl; amps_.resize(R.size()); const RCMatrix pEvecs(MatrixTools::pinv(evecs_)); // amps_ = pEvecs*R; for (label i = 0; i < amps_.size(); ++i) { for (label j = 0; j < R.size(); ++j) { amps_[i] += pEvecs(i,j)*R[j]; } } } Pstream::broadcast(amps_); } void Foam::DMDModels::STDMD::magnitudes() { if (Pstream::master()) { Info<< tab << "Computing magnitudes" << endl; mags_.resize(amps_.size()); Info<< tab << "Sorting modes with "; switch (modeSorter_) { case modeSorterType::FIRST_SNAPSHOT: { Info<< "method of first snapshot" << endl; std::transform ( amps_.cbegin(), amps_.cend(), mags_.begin(), [&](const complex& val){ return mag(val); } ); break; } case modeSorterType::KIEWAT: { Info<< "modified weighted amplitude scaling method" << endl; // Eigendecomposition returns evecs with // the unity norm, hence "modeNorm = 1" const scalar modeNorm = 1; const scalar pr = 1; List w(step_); std::iota(w.begin(), w.end(), 1); w = sin(twoPi/step_*(w - 1 - 0.25*step_))*pr + pr; forAll(mags_, i) { mags_[i] = sorter(w, amps_[i], evals_[i], modeNorm); } break; } case modeSorterType::KOU_ZHANG: { Info<< "weighted amplitude scaling method" << endl; const scalar modeNorm = 1; const List w(step_, 1); forAll(mags_, i) { mags_[i] = sorter(w, amps_[i], evals_[i], modeNorm); } break; } default: break; } Info<< tab << "Computing magnitude indices" << endl; magsi_ = freqsi_; auto descend = [&](const label i1, const label i2) { return !(mags_[i1] < mags_[i2]); }; std::sort(magsi_.begin(), magsi_.end(), descend); } Pstream::broadcast(mags_); Pstream::broadcast(magsi_); } Foam::scalar Foam::DMDModels::STDMD::sorter ( const List& weight, const complex& amplitude, const complex& eigenvalue, const scalar modeNorm ) const { // Omit eigenvalues with very large or very small mags if (!(mag(eigenvalue) < GREAT && mag(eigenvalue) > VSMALL)) { Info<< " Returning zero magnitude for mag(eigenvalue) = " << mag(eigenvalue) << endl; return 0; } // Omit eigenvalue-STDMD step combinations that pose a risk of overflow if (mag(eigenvalue)*step_ > sortLimiter_) { Info<< " Returning zero magnitude for" << " mag(eigenvalue) = " << mag(eigenvalue) << " current index = " << step_ << " sortLimiter = " << sortLimiter_ << endl; return 0; } scalar magnitude = 0; for (label j = 0; j < step_; ++j) { magnitude += weight[j]*modeNorm*mag(amplitude*pow(eigenvalue, j + 1)); } return magnitude; } bool Foam::DMDModels::STDMD::dynamics() { SMatrix Atilde(reducedKoopmanOperator()); G_.clear(); if(!eigendecomposition(Atilde)) { return false; } Atilde.clear(); frequencies(); amplitudes(); magnitudes(); return true; } bool Foam::DMDModels::STDMD::modes() { Info<< tab << "Computing modes" << endl; bool processed = false; processed = processed || modes(); processed = processed || modes(); processed = processed || modes(); processed = processed || modes(); processed = processed || modes(); if (!processed) { return false; } return true; } void Foam::DMDModels::STDMD::writeToFile(const word& fileName) const { Info<< tab << "Writing objects of dynamics" << endl; // Write objects of dynamics { autoPtr osPtr = newFileAtTime ( fileName + "_" + fieldName_, mesh_.time().timeOutputValue() ); OFstream& os = osPtr.ref(); writeFileHeader(os); for (const auto& i : labelRange(0, freqs_.size())) { os << freqs_[i] << tab << mags_[i] << tab << amps_[i].real() << tab << amps_[i].imag() << tab << evals_[i].real() << tab << evals_[i].imag() << endl; } } Info<< tab << "Ends output processing for field: " << fieldName_ << endl; } void Foam::DMDModels::STDMD::writeFileHeader(Ostream& os) const { writeHeader(os, "DMD output"); writeCommented(os, "Frequency"); writeTabbed(os, "Magnitude"); writeTabbed(os, "Amplitude (real)"); writeTabbed(os, "Amplitude (imag)"); writeTabbed(os, "Eigenvalue (real)"); writeTabbed(os, "Eigenvalue (imag)"); os << endl; } void Foam::DMDModels::STDMD::filter() { Info<< tab << "Filtering objects of dynamics" << endl; // Filter objects according to magsi filterIndexed(evals_, magsi_); filterIndexed(evecs_, magsi_); filterIndexed(freqs_, magsi_); filterIndexed(amps_, magsi_); filterIndexed(mags_, magsi_); // Clip objects if need be (assuming objects have the same len) if (freqs_.size() > nModes_) { evals_.resize(nModes_); evecs_.resize(evecs_.m(), nModes_); freqs_.resize(nModes_); amps_.resize(nModes_); mags_.resize(nModes_); } } // * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * // Foam::DMDModels::STDMD::STDMD ( const fvMesh& mesh, const word& name, const dictionary& dict ) : DMDModel(mesh, name, dict), modeSorter_ ( modeSorterTypeNames.getOrDefault ( "modeSorter", dict, modeSorterType::FIRST_SNAPSHOT ) ), Q_(), G_(), Qupper_(1, 1, Zero), Qlower_(1, 1, Zero), RxInv_(1, 1, Zero), evals_(Zero), evecs_(1, 1, Zero), freqs_(Zero), freqsi_(), amps_(Zero), mags_(Zero), magsi_(Zero), patches_ ( dict.getOrDefault ( "patches", dict.found("patch") ? wordRes(1,dict.get("patch")) : wordRes() ) ), fieldName_(dict.get("field")), timeName0_(), minBasis_(0), minEval_(0), dt_(0), sortLimiter_(500), fMin_(0), fMax_(pTraits