///////////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2025- Equinor ASA // // ResInsight 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. // // ResInsight 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 at // for more details. // ///////////////////////////////////////////////////////////////////////////////// #include "RifVtkReader.h" #include "RifVtkImportUtil.h" #include "RigFemPart.h" #include "RigFemPartCollection.h" #include "RigFemResultPosEnum.h" #include "RigFemTypes.h" #include "RiaLogging.h" #include "RiaQStringFormatter.h" #include "cafAssert.h" #include "cafProgressInfo.h" #include #include #include namespace RifVtkFieldNames { constexpr const char* separator = "__"; constexpr const char* S = "S_VTK"; constexpr const char* S11 = "S11"; constexpr const char* S22 = "S22"; constexpr const char* S33 = "S33"; constexpr const char* S12 = "S12"; constexpr const char* S13 = "S13"; constexpr const char* S23 = "S23"; constexpr const char* E = "E_VTK"; constexpr const char* E11 = "E11"; constexpr const char* E22 = "E22"; constexpr const char* E33 = "E33"; constexpr const char* E12 = "E12"; constexpr const char* E13 = "E13"; constexpr const char* E23 = "E23"; } // namespace RifVtkFieldNames //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RifVtkReader::RifVtkReader() { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RifVtkReader::~RifVtkReader() { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RifVtkReader::close() { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RifVtkReader::populateDerivedResultNames() const { return false; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RifVtkReader::openFile( const std::string& fileName, std::string* errorMessage ) { std::filesystem::path filePath( fileName ); auto dataset = RifVtkImportUtil::parsePvdDatasets( filePath ); if ( dataset.empty() ) { RiaLogging::error( "No timesteps found" ); m_inputPath.clear(); return false; } else { RiaLogging::info( std::format( "Found {} timesteps", dataset.size() ) ); m_inputPath = filePath; return true; } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RifVtkReader::isOpen() const { return !m_inputPath.empty(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RifVtkReader::readFemParts( RigFemPartCollection* femParts ) { CAF_ASSERT( femParts ); // The key in the maps is the part ID std::map parts; std::map>> nodes; std::map>>> elements; std::map>>> elementSets; std::map>>> properties; auto result = read( m_inputPath, parts, nodes, elements, elementSets, properties, m_displacements, m_stepNames ); if ( !result ) { RiaLogging::error( result.error() ); return false; } auto elementType = result.value(); if ( m_stepNames.empty() ) m_stepNames.push_back( "Geostatic" ); RiaLogging::debug( QString( "Read FEM parts: %1, steps: %2, element type: %3" ) .arg( parts.size() ) .arg( m_stepNames.size() ) .arg( QString::fromStdString( RigFemTypes::elementTypeText( elementType ) ) ) .toStdString() ); if ( !RigFemTypes::is8NodeElement( elementType ) ) { RiaLogging::error( "Unsupported element type." ); return false; } caf::ProgressInfo modelProgress( parts.size() * 2, "Reading Inp Parts" ); for ( const auto& [partId, partName] : parts ) { modelProgress.setProgressDescription( QString::fromStdString( partName ) + ": Reading Nodes" ); RigFemPart* femPart = new RigFemPart; femPart->setName( partName ); // Extract nodes std::vector> partNodes = nodes[partId]; std::map nodeIdToIdxMap; int nodeCount = static_cast( partNodes.size() ); femPart->nodes().nodeIds.resize( nodeCount ); femPart->nodes().coordinates.resize( nodeCount ); for ( int nIdx = 0; nIdx < nodeCount; ++nIdx ) { auto [nodeId, pos] = partNodes[nIdx]; femPart->nodes().nodeIds[nIdx] = nodeId; femPart->nodes().coordinates[nIdx].set( pos[0], pos[1], pos[2] ); nodeIdToIdxMap[nodeId] = nIdx; } modelProgress.incrementProgress(); modelProgress.setProgressDescription( QString::fromStdString( partName ) + ": Reading Elements" ); // Extract elements std::vector>> partElements = elements[partId]; int elmCount = static_cast( partElements.size() ); femPart->preAllocateElementStorage( elmCount ); std::vector indexBasedConnectivities; std::map elementIdToIdxMap; for ( int elmIdx = 0; elmIdx < elmCount; ++elmIdx ) { auto [elmId, nodesInElement] = partElements[elmIdx]; elementIdToIdxMap[elmId] = elmIdx; int nodeCount = RigFemTypes::elementNodeCount( elementType ); indexBasedConnectivities.resize( nodeCount ); for ( int lnIdx = 0; lnIdx < nodeCount; ++lnIdx ) { indexBasedConnectivities[lnIdx] = nodeIdToIdxMap[nodesInElement[lnIdx]]; } femPart->appendElement( elementType, elmId, indexBasedConnectivities.data() ); } // read element sets auto& elementSetsForPart = elementSets[partId]; for ( auto& [setName, elementSet] : elementSetsForPart ) { femPart->addElementSet( setName, elementSet ); } // VTK coordinates are negated to match ResInsight internal convention (z positive downward), // which flips the IJK directions. Invert all so IJK=0 is at the correct corner. femPart->setInvertIJKInStructGrid( true ); femPart->setElementPartId( femParts->partCount() ); femParts->addFemPart( femPart ); modelProgress.incrementProgress(); } size_t elementCount = femParts->part( 0 )->elementCount(); readScalarData( femParts, parts, properties, elementCount ); return true; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::expected RifVtkReader::read( const std::filesystem::path& filePath, std::map& parts, std::map>>& nodes, std::map>>>& elements, std::map>>>& elementSets, std::map>>>& properties, std::map>>& displacements, std::vector& stepNames ) { RigElementType elementType = RigElementType::UNKNOWN_ELM_TYPE; int partId = 0; std::string partName; int stepId = -1; std::string stepName; int timeSteps = 0; auto dataset = RifVtkImportUtil::parsePvdDatasets( filePath ); // Only one part supported parts[partId] = "Model"; // elementSets[partId] = {}; bool isFirst = true; for ( const RifVtkImportUtil::PvdDataset& d : dataset ) { stepNames.push_back( "Dataset " + std::to_string( d.timestep ) ); pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file( d.filepath.string().c_str() ); if ( !result ) return std::unexpected( "File did load: " + d.filepath.string() ); auto root = doc.child( "VTKFile" ); if ( !root ) return std::unexpected( "Missing VTKFile tag: " + d.filepath.string() ); auto grid = root.child( "UnstructuredGrid" ); if ( !grid ) return std::unexpected( "Missing UnstructedGrid tag: " + d.filepath.string() ); auto piece = grid.child( "Piece" ); if ( !piece ) return std::unexpected( "Missing Piece tag: " + d.filepath.string() ); // Use the geometry from the first time step only. // The geometry is assumed to not change between time steps. if ( isFirst ) { // Read points std::vector vertices = RifVtkImportUtil::readPoints( piece ); if ( vertices.empty() ) return std::unexpected( "No points found: " + d.filepath.string() ); // Read connectivity std::vector connectivity = RifVtkImportUtil::readConnectivity( piece ); if ( connectivity.empty() ) return std::unexpected( "No connectivity found: " + d.filepath.string() ); if ( std::ranges::any_of( connectivity, [&vertices]( unsigned index ) { return index >= vertices.size(); } ) ) { return std::unexpected( "Connectivity refers to a point that does not exist: " + d.filepath.string() ); } std::vector> partNodes; for ( int i = 0; i < static_cast( vertices.size() ); i++ ) { partNodes.push_back( { i, vertices[i] } ); } nodes[partId] = partNodes; std::vector>> partElements; int numNodesPerElement = 8; int numCells = static_cast( connectivity.size() / numNodesPerElement ); for ( int i = 0; i < numCells; i++ ) { std::vector els( numNodesPerElement ); for ( int x = 0; x < numNodesPerElement; x++ ) { els[x] = connectivity[i * numNodesPerElement + x]; } partElements.emplace_back( i, els ); } elements[partId] = partElements; elementType = RigElementType::HEX8P; } // Read displacements for all timesteps std::vector partDisplacements = RifVtkImportUtil::readDisplacements( piece ); if ( partDisplacements.empty() ) return std::unexpected( "No displacements found: " + d.filepath.string() ); const std::map> partProperties = RifVtkImportUtil::readProperties( piece ); RiaLogging::info( QString( "Found %1 displacements and %2 properties for timestep %3." ) .arg( partDisplacements.size() ) .arg( partProperties.size() ) .arg( d.timestep ) .toStdString() ); properties[partId].push_back( partProperties ); displacements[partId].push_back( partDisplacements ); isFirst = false; } return elementType; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RifVtkReader::allStepNames() const { return m_stepNames; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RifVtkReader::filteredStepNames() const { // no filter supported return RifVtkReader::allStepNames(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RifVtkReader::frameTimes( int stepIndex ) const { // only one frame from INP file std::vector frameValues( { 1.0 } ); return frameValues; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- int RifVtkReader::frameCount( int stepIndex ) const { return static_cast( frameTimes( stepIndex ).size() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RifVtkReader::elementSetNames( int partIndex, std::string partName ) { if ( partIndex >= (int)m_partElementSetNames.size() ) return {}; return m_partElementSetNames.at( partIndex ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RifVtkReader::elementSet( int partIndex, std::string partName, int setIndex ) { // TODO: not implemented yet std::vector elementIndexes; return elementIndexes; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::map> RifVtkReader::scalarNodeFieldAndComponentNames() { std::map> retVal; for ( auto& entry : m_propertyPartDataNodes ) { retVal[entry.first] = {}; } retVal["U"] = { "U1", "U2", "U3" }; return retVal; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::map> RifVtkReader::scalarElementFieldAndComponentNames() { using namespace RifVtkFieldNames; std::map> retVal; for ( auto& entry : m_propertyPartDataElements ) { // Skip internal S/E component keys (stored as "S_VTK__S11", "E_VTK__E11", etc.) const std::string& key = entry.first; if ( key.starts_with( std::string( S ) + separator ) || key.starts_with( std::string( E ) + separator ) ) continue; retVal[key] = {}; } // Add S field with components if any S component data was loaded if ( m_propertyPartDataElements.count( std::string( S ) + separator + S11 ) > 0 ) { retVal[S] = { S11, S22, S33, S12, S13, S23 }; } // Add E field with components if any E component data was loaded if ( m_propertyPartDataElements.count( std::string( E ) + separator + E11 ) > 0 ) { retVal[E] = { E11, E22, E33, E12, E13, E23 }; } return retVal; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::map> RifVtkReader::scalarElementNodeFieldAndComponentNames() { return {}; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::map> RifVtkReader::scalarIntegrationPointFieldAndComponentNames() { return {}; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RifVtkReader::readDisplacements( int partIndex, int stepIndex, int frameIndex, std::vector* displacements ) { CAF_ASSERT( displacements ); if ( auto it = m_displacements.find( partIndex ); it != m_displacements.end() ) { *displacements = it->second[stepIndex]; } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RifVtkReader::readField( RigFemResultPosEnum resultType, const std::string& fieldName, int partIndex, int stepIndex, std::vector*>* resultValues ) { CAF_ASSERT( resultValues ); auto dataMap = propertyDataMap( resultType ); if ( dataMap == nullptr ) return; if ( dataMap->count( fieldName ) == 0 ) return; // is there only a static result? Use it for all steps. if ( ( *dataMap )[fieldName].count( stepIndex ) == 0 ) { if ( ( *dataMap )[fieldName].count( -1 ) == 0 ) return; stepIndex = -1; } if ( ( *dataMap )[fieldName][stepIndex].count( partIndex ) == 0 ) return; auto dataSize = ( *dataMap )[fieldName][stepIndex][partIndex].size(); ( *resultValues )[0]->resize( dataSize ); std::vector* singleComponentValues = ( *resultValues )[0]; for ( size_t i = 0; i < dataSize; i++ ) { ( *singleComponentValues )[i] = (float)( *dataMap )[fieldName][stepIndex][partIndex][i]; } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RifVtkReader::readNodeField( const std::string& fieldName, int partIndex, int stepIndex, int frameIndex, std::vector*>* resultValues ) { CAF_ASSERT( resultValues ); if ( fieldName == "U" ) { std::vector disp; readDisplacements( partIndex, stepIndex, frameIndex, &disp ); if ( disp.empty() ) return; size_t n = disp.size(); for ( int comp = 0; comp < 3 && comp < static_cast( resultValues->size() ); comp++ ) { if ( ( *resultValues )[comp] == nullptr ) continue; ( *resultValues )[comp]->resize( n ); for ( size_t i = 0; i < n; i++ ) { ( *( *resultValues )[comp] )[i] = disp[i][comp]; } } return; } readField( RigFemResultPosEnum::RIG_NODAL, fieldName, partIndex, stepIndex, resultValues ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RifVtkReader::readElementField( const std::string& fieldName, int partIndex, int stepIndex, int frameIndex, std::vector*>* resultValues ) { // S and E fields have 6 components stored as individual internal keys with a separator if ( fieldName == RifVtkFieldNames::S || fieldName == RifVtkFieldNames::E ) { using namespace RifVtkFieldNames; static const std::vector sComponents = { S11, S22, S33, S12, S13, S23 }; static const std::vector eComponents = { E11, E22, E33, E12, E13, E23 }; const auto& components = ( fieldName == RifVtkFieldNames::S ) ? sComponents : eComponents; size_t count = std::min( components.size(), resultValues->size() ); for ( size_t i = 0; i < count; i++ ) { std::string internalKey = fieldName + separator + components[i]; std::vector*> singleComp = { ( *resultValues )[i] }; readField( RigFemResultPosEnum::RIG_ELEMENT, internalKey, partIndex, stepIndex, &singleComp ); } return; } readField( RigFemResultPosEnum::RIG_ELEMENT, fieldName, partIndex, stepIndex, resultValues ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RifVtkReader::readElementNodeField( const std::string& fieldName, int partIndex, int stepIndex, int frameIndex, std::vector*>* resultValues ) { readField( RigFemResultPosEnum::RIG_ELEMENT_NODAL, fieldName, partIndex, stepIndex, resultValues ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RifVtkReader::readIntegrationPointField( const std::string& fieldName, int partIndex, int stepIndex, int frameIndex, std::vector*>* resultValues ) { readField( RigFemResultPosEnum::RIG_INTEGRATION_POINT, fieldName, partIndex, stepIndex, resultValues ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RifVtkReader::readScalarData( RigFemPartCollection* femParts, std::map& parts, const std::map>>>& properties, size_t numElements ) { for ( auto [partId, partName] : parts ) { if ( auto partProperties = properties.find( partId ); partProperties != properties.end() ) { int stepId = 0; for ( const auto& propertiesPerTimeStep : partProperties->second ) { for ( const auto& [propertyName, values] : propertiesPerTimeStep ) { RigFemResultPosEnum resultType = values.size() == numElements ? RigFemResultPosEnum::RIG_ELEMENT : RigFemResultPosEnum::RIG_NODAL; // TODO: handle other result types... if ( resultType == RigFemResultPosEnum::RIG_ELEMENT ) { auto map = propertyDataMap( resultType ); if ( map == nullptr ) continue; if ( map->count( propertyName ) == 0 ) { ( *map )[propertyName] = {}; } if ( ( *map )[propertyName].count( stepId ) == 0 ) { ( *map )[propertyName][stepId] = {}; } for ( int partId = 0; partId < femParts->partCount(); partId++ ) { ( *map )[propertyName][stepId][partId] = {}; size_t dataSize = 0; if ( resultType == RigFemResultPosEnum::RIG_NODAL ) { dataSize = femParts->part( partId )->nodeCount(); } if ( resultType == RigFemResultPosEnum::RIG_ELEMENT ) { dataSize = femParts->part( partId )->elementCount(); } std::vector valuesAsDouble( dataSize, 0.0 ); // TODO: fix annoying conversion from float to double for ( size_t i = 0; i < dataSize; i++ ) { valuesAsDouble[i] = values[i]; } ( *map )[propertyName][stepId][partId] = valuesAsDouble; } } else if ( values.size() == 3 * numElements ) { auto map = propertyDataMap( RigFemResultPosEnum::RIG_ELEMENT ); if ( map == nullptr ) continue; // OPM Flow writes a symmetric 3x3 tensor as three rows "stress[0..2]" of 3 values each. // The upper triangle is read. z is negated on import, so the xz and yz components change sign. struct StressStrainMapping { std::string vtkName; int column; std::string fieldName; std::string componentName; double sign; }; using namespace RifVtkFieldNames; static const std::vector mappings = { { "stress[0]", 0, S, S11, 1.0 }, { "stress[0]", 1, S, S12, 1.0 }, { "stress[0]", 2, S, S13, -1.0 }, { "stress[1]", 1, S, S22, 1.0 }, { "stress[1]", 2, S, S23, -1.0 }, { "stress[2]", 2, S, S33, 1.0 }, { "strain[0]", 0, E, E11, 1.0 }, { "strain[0]", 1, E, E12, 1.0 }, { "strain[0]", 2, E, E13, -1.0 }, { "strain[1]", 1, E, E22, 1.0 }, { "strain[1]", 2, E, E23, -1.0 }, { "strain[2]", 2, E, E33, 1.0 }, }; for ( const auto& mapping : mappings ) { if ( propertyName != mapping.vtkName ) continue; std::string internalKey = mapping.fieldName + separator + mapping.componentName; for ( int partIdx = 0; partIdx < femParts->partCount(); partIdx++ ) { size_t dataSize = femParts->part( partIdx )->elementCount(); std::vector compValues( dataSize, 0.0 ); for ( size_t i = 0; i < dataSize; i++ ) { compValues[i] = mapping.sign * values[i * 3 + mapping.column]; } ( *map )[internalKey][stepId][partIdx] = compValues; } } } } stepId++; } } } } //-------------------------------------------------------------------------------------------------- /// Map keys: result name / time step / part id //-------------------------------------------------------------------------------------------------- std::map>>>* RifVtkReader::propertyDataMap( RigFemResultPosEnum resultType ) { if ( resultType == RigFemResultPosEnum::RIG_ELEMENT ) return &m_propertyPartDataElements; if ( resultType == RigFemResultPosEnum::RIG_NODAL ) return &m_propertyPartDataNodes; return nullptr; }