/*---------------------------------------------------------------------------*\ ========= | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox \\ / O peration | \\ / A nd | www.openfoam.com \\/ M anipulation | ------------------------------------------------------------------------------- Copyright (C) 2016-2024 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 "ensightCase.H" #include "Time.H" #include "cloud.H" #include "IOmanip.H" #include "OSstream.H" #include #include // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * // const char* Foam::ensightCase::dataDirName = "data"; const char* Foam::ensightCase::geometryName = "geometry"; // * * * * * * * * * * * * * Static Member Functions * * * * * * * * * * * * // Foam::word Foam::ensightCase::mask(const int nwidth) { if (nwidth < 1) { return word(); } return word(std::string(nwidth, '*'), false); // stripping=false } Foam::word Foam::ensightCase::padded(const int nwidth, const label index) { if (nwidth < 1) { return Foam::name(index); } std::ostringstream oss; oss << std::setfill('0') << std::setw(nwidth) << index; return word(oss.str(), false); // stripping=false } void Foam::ensightCase::setTimeFormat ( OSstream& os, IOstreamOption::floatFormat timeFmt, const int timePrec ) { os.setf(std::ios_base::left); os.setf ( std::ios_base::fmtflags(timeFmt), std::ios_base::floatfield ); if (timePrec > 0) { os.precision(timePrec); } } void Foam::ensightCase::setTimeFormat ( OSstream& os, const ensightCase::options& opts ) { os.setf(std::ios_base::left); os.setf ( std::ios_base::fmtflags(opts.timeFormat()), std::ios_base::floatfield ); os.precision(opts.timePrecision()); } void Foam::ensightCase::printTimeset ( OSstream& os, const label ts, const scalar timeValue ) { os << "time set: " << ts << nl << "number of steps: " << 1 << nl; // Single value - starts at index 0 os << "filename start number: 0" << nl << "filename increment: 1" << nl << "time values:" << nl; os << " " << timeValue << nl << nl; } void Foam::ensightCase::printTimeset ( OSstream& os, const label ts, const UList& values ) { label pos_(0); os << "time set: " << ts << nl << "number of steps: " << values.size() << nl; // Assume contiguous numbering - starts at index 0 os << "filename start number: 0" << nl << "filename increment: 1" << nl; os << "time values:" << nl; pos_ = 0; for (const scalar val : values) { if (pos_ == 6) { os << nl; pos_ = 0; } ++pos_; os << ' ' << setf(ios_base::right) << setw(12) << val; } os << nl << nl; } void Foam::ensightCase::printTimeset ( OSstream& os, const label ts, const UList& values, const bitSet& indices ) { label pos_(0); // Check if continuous numbering can be used if ( values.empty() || (indices.size() == values.size() && indices.all()) ) { // Can simply emit as 0-based with increment printTimeset(os, ts, values); return; } // Generate time set os << "time set: " << ts << nl << "number of steps: " << indices.count() << nl; os << "filename numbers:" << nl; pos_ = 0; for (const label idx : indices) { if (pos_ == 6) { os << nl; pos_ = 0; } ++pos_; os << ' ' << setf(ios_base::right) << setw(8) << idx; } os << nl; os << "time values:" << nl; pos_ = 0; for (const label idx : indices) { if (pos_ == 6) { os << nl; pos_ = 0; } ++pos_; os << ' ' << setf(ios_base::right) << setw(12) << values[idx]; } os << nl << nl; } // * * * * * * * * * * * * * Private Functions * * * * * * * * * * * * * * // Foam::fileName Foam::ensightCase::dataDir() const { return ensightDir_/dataDirName; } void Foam::ensightCase::initialize() { if (UPstream::master()) { // EnSight and EnSight/data directories must exist // We may wish to retain old data // eg, convert new results or a particular time interval // OR remove everything if (Foam::isDir(ensightDir_)) { if (options_->overwrite()) { Foam::rmDir(ensightDir_); } else { DetailInfo << "Warning: re-using existing directory" << nl << " " << ensightDir_ << endl; } } // Create ensight and data directories Foam::mkDir(dataDir()); // The case file is always ASCII os_.reset(new OFstream(ensightDir_/caseName_, IOstreamOption::ASCII)); ensightCase::setTimeFormat(*os_, *options_); // Format options writeHeader(); } } Foam::label Foam::ensightCase::checkTimeset(const labelHashSet& lookup) const { // assume the worst label ts = -1; // work on a copy labelHashSet tsTimes(lookup); tsTimes.erase(-1); if (tsTimes.empty()) { // no times needed ts = 0; } else if (tsTimes.size() == timesUsed_.size()) { forAllConstIters(timesUsed_, iter) { tsTimes.erase(iter.key()); } // OR // tsTimes.unset(timesUsed_.toc()); if (tsTimes.empty()) { ts = 1; // can use timeset 1 } } return ts; } void Foam::ensightCase::writeHeader() const { if (os_) // True on master only { this->rewind(); *os_ << "FORMAT" << nl << "type: ensight gold" << nl; } } Foam::scalar Foam::ensightCase::writeTimeset() const { const label ts = 1; const labelList indices(timesUsed_.sortedToc()); label count = indices.size(); // correct for negative starting values scalar timeCorrection = timesUsed_[indices[0]]; if (timeCorrection < 0) { timeCorrection = -timeCorrection; Info<< "Correcting time values. Adding " << timeCorrection << endl; } else { timeCorrection = 0; } *os_ << "time set: " << ts << nl << "number of steps: " << count << nl; if (indices[0] == 0 && indices[count-1] == count-1) { // looks to be contiguous numbering *os_ << "filename start number: " << 0 << nl << "filename increment: " << 1 << nl; } else { *os_ << "filename numbers:" << nl; count = 0; for (const label idx : indices) { *os_ << ' ' << setw(12) << idx; if (++count % 6 == 0) { *os_ << nl; } } if (count) { *os_ << nl; } } *os_ << "time values:" << nl; count = 0; for (const label idx : indices) { *os_ << ' ' << setw(12) << timesUsed_[idx] + timeCorrection; if (++count % 6 == 0) { *os_ << nl; } } if (count) { *os_ << nl; } return timeCorrection; } void Foam::ensightCase::writeTimeset ( const label ts, const labelHashSet& lookup, const scalar timeCorrection ) const { // Make a copy labelHashSet hashed(lookup); hashed.erase(-1); const labelList indices(hashed.sortedToc()); label count = indices.size(); *os_ << "time set: " << ts << nl << "number of steps: " << count << nl << "filename numbers:" << nl; count = 0; for (const label idx : indices) { *os_ << ' ' << setw(12) << idx; if (++count % 6 == 0) { *os_ << nl; } } if (count) { *os_ << nl; } *os_ << "time values:" << nl; count = 0; for (const label idx : indices) { *os_ << ' ' << setw(12) << timesUsed_[idx] + timeCorrection; if (++count % 6 == 0) { *os_ << nl; } } if (count) { *os_ << nl; } } void Foam::ensightCase::noteGeometry(const bool moving) const { if (moving) { geomTimes_.insert(timeIndex_); } else { geomTimes_.insert(-1); } changed_ = true; } void Foam::ensightCase::noteCloud(const word& cloudName) const { // Force into existence if (!cloudVars_.found(cloudName)) { cloudVars_.emplace(cloudName); } cloudTimes_.insert(timeIndex_); changed_ = true; } void Foam::ensightCase::noteCloud ( const word& cloudName, const word& varName, const char* ensightType ) const { if (cloudVars_.found(cloudName)) { if (cloudVars_[cloudName].insert(varName, ensightType)) { changed_ = true; } } else { FatalErrorInFunction << "Tried to add a cloud variable for writing" << " - without having added a cloud" << abort(FatalError); } } void Foam::ensightCase::noteVariable ( const word& varName, const char* ensightType ) const { if (variables_.insert(varName, ensightType)) { changed_ = true; } } Foam::autoPtr Foam::ensightCase::createDataFile ( const word& name ) const { if (UPstream::master()) { // The data/ITER subdirectory must exist // Note that data/ITER is indeed a valid ensight::FileName const fileName outdir = dataDir()/padded(timeIndex_); Foam::mkDir(outdir); return autoPtr::New(outdir, name, format()); } return nullptr; } Foam::autoPtr Foam::ensightCase::createCloudFile ( const word& cloudName, const word& name ) const { if (UPstream::master()) { // Write // eg -> "data/********/lagrangian//positions" // or -> "lagrangian//********/positions" // TODO? check that cloudName is a valid ensight filename const fileName outdir = ( separateCloud() ? (ensightDir_ / cloud::prefix / cloudName / padded(timeIndex_)) : (dataDir() / padded(timeIndex_) / cloud::prefix / cloudName) ); Foam::mkDir(outdir); // should be unnecessary after newCloud() return autoPtr::New(outdir, name, format()); } return nullptr; } // * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * // Foam::ensightCase::ensightCase ( const fileName& ensightDir, const word& caseName, const ensightCase::options& opts ) : options_(new options(opts)), os_(nullptr), ensightDir_(ensightDir), caseName_(caseName + ".case"), changed_(false), timeIndex_(0), timeValue_(0) { initialize(); } Foam::ensightCase::ensightCase ( const fileName& ensightDir, const word& caseName, const IOstreamOption::streamFormat fmt ) : options_(new options(fmt)), os_(nullptr), ensightDir_(ensightDir), caseName_(caseName + ".case"), changed_(false), timeIndex_(0), timeValue_(0) { initialize(); } // * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * // void Foam::ensightCase::nextTime(const scalar value) { // use next available index setTime(value, timesUsed_.size()); } void Foam::ensightCase::nextTime(const instant& t) { nextTime(t.value()); } void Foam::ensightCase::setTime(const scalar value, const label index) { timeIndex_ = index; timeValue_ = value; if (UPstream::master()) { // The data/ITER subdirectory must exist // Note that data/ITER is indeed a valid ensight::FileName const fileName outdir = dataDir()/padded(timeIndex_); Foam::mkDir(outdir); // place a timestamp in the directory for future reference OFstream timeStamp(outdir/"time"); timeStamp << "# index time" << nl << outdir.name() << ' ' << timeValue_ << nl; } // Record of time index/value used timesUsed_.set(index, value); } void Foam::ensightCase::setTime(const instant& t, const label index) { setTime(t.value(), index); } void Foam::ensightCase::write() const { if (!os_) return; // master only // geometry timeset const bool staticGeom = (geomTimes_.size() == 1 && geomTimes_.found(-1)); label tsGeom = staticGeom ? 0 : checkTimeset(geomTimes_); // geometry index, when mesh is not moving but stored under data/XXX/ label meshIndex = -1; // cloud timeset label tsCloud = checkTimeset(cloudTimes_); // Increment time-sets to the correct indices if (tsGeom < 0) { tsGeom = 2; // Next available timeset // Saved under data/XXX/geometry, but not actually moving if (geomTimes_.size() == 1) { tsGeom = 0; meshIndex = *(geomTimes_.begin()); } } if (tsCloud < 0) { tsCloud = 1 + std::max(label(1), tsGeom); // Next available timeset } writeHeader(); // data mask: eg "data/******" const fileName dataMask = (dataDirName/mask()); // // GEOMETRY // if (!geomTimes_.empty() || !cloudTimes_.empty()) { // start of variables *os_ << nl << "GEOMETRY" << nl; } if (staticGeom) { // Static mesh: store under data/constant/geometry *os_ << setw(16) << "model:" << (dataDirName/word("constant")/geometryName).c_str() << nl; } else if (meshIndex >= 0) { // Not really moving, but stored under data/XXXX/geometry *os_ << setw(16) << "model:" << (dataDirName/padded(meshIndex)/geometryName).c_str() << nl; } else if (!geomTimes_.empty()) { // Moving *os_ << word::printf("model: %-9d", tsGeom) // width 16 (no quotes) << (dataMask/geometryName).c_str() << nl; } // Clouds and cloud variables const wordList cloudNames(cloudVars_.sortedToc()); for (const word& cloudName : cloudNames) { const fileName masked = ( separateCloud() ? (cloud::prefix / cloudName / mask()) : (dataMask / cloud::prefix / cloudName) ); *os_ << word::printf("measured: %-6d", tsCloud) // width 16 (no quotes) << (masked/"positions").c_str() << nl; } // // VARIABLE // if (variables_.size() || cloudVars_.size()) { // Start of variables *os_ << nl << "VARIABLE" << nl; } // Field variables (always use timeset 1) // NB: The output file name is stricter than the variable name for (const word& varName : variables_.sortedToc()) { const string& ensType = variables_[varName]; *os_ << ensType.c_str() << ( (nodeVariables_.found(varName) || nodeValues()) ? " per node: 1 " // time-set 1 : " per element: 1 " // time-set 1 ) << setw(15) << varName << ' ' << (dataMask/ensight::FileName(varName)).c_str() << nl; } // Clouds and cloud variables (using cloud timeset) // Write // as -> "data/********/lagrangian//positions" // or -> "lagrangian//********/positions" // NB: The output file name is stricter than the variable name label cloudNo = 0; for (const word& cloudName : cloudNames) { const fileName masked = ( separateCloud() ? (cloud::prefix / cloudName / mask()) : (dataMask / cloud::prefix / cloudName) ); const HashTable& vars = cloudVars_[cloudName]; for (const word& varName : vars.sortedToc()) { const string& ensType = vars[varName]; // prefix variables with 'c' (cloud) and cloud index *os_ << ensType.c_str() << " per " << word::printf("measured node: %-5d", tsCloud) // width 20 << setw(15) << ("c" + Foam::name(cloudNo) + varName).c_str() << ' ' << (masked/ensight::FileName(varName)).c_str() << nl; } ++cloudNo; } // // TIME // if (!timesUsed_.empty()) { *os_ << nl << "TIME" << nl; // timeset 1 const scalar timeCorrection = writeTimeset(); // timeset geometry if (tsGeom > 1) { writeTimeset(tsGeom, geomTimes_, timeCorrection); } // timeset cloud if (tsCloud > 1) { writeTimeset(tsCloud, cloudTimes_, timeCorrection); } *os_ << "# end" << nl; } *os_ << flush; changed_ = false; } Foam::autoPtr Foam::ensightCase::newGeometry ( bool moving ) const { autoPtr filePtr; if (UPstream::master()) { // Set the path of the ensight file fileName path; if (moving) { // Moving mesh: write as "data/********/geometry" path = dataDir()/padded(timeIndex_); } else { // Static mesh: write as "data/constant/geometry" path = dataDir()/word("constant"); } Foam::mkDir(path); noteGeometry(moving); // note for later use filePtr.reset(new ensightGeoFile(path, geometryName, format())); // Before 2024-05 also implicitly called beginGeometry() } return filePtr; } Foam::autoPtr Foam::ensightCase::newCloud ( const word& cloudName ) const { autoPtr filePtr; if (UPstream::master()) { filePtr = createCloudFile(cloudName, "positions"); auto& os = filePtr(); // Tag binary format (just like geometry files) os.writeBinaryHeader(); // Description os.write(cloud::prefix/cloudName); os.newline(); noteCloud(cloudName); // note for later use } return filePtr; } void Foam::ensightCase::rewind() const { if (os_) // master only { os_->stdStream().seekp(0, std::ios_base::beg); } } Foam::Ostream& Foam::ensightCase::printInfo(Ostream& os) const { os << "Ensight case:" << nl << " path: " << ensightDir_ << nl << " name: " << caseName_ << nl << " format: " << format() << nl; if (nodeValues()) { os << " values per node" << nl; } return os; } // ************************************************************************* //