Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 227 additions & 14 deletions applications/MedApplication/custom_io/med_model_part_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,14 @@ auto GetGroupsByFamily(
std::vector<std::string> group_names(num_groups);
// split the goup names
for (int i = 0; i < num_groups; i++) {
group_names[i] = StringUtilities::Trim(c_group_names.substr(i * MED_LNAME_SIZE, MED_LNAME_SIZE), /*RemoveNullChar=*/true);
std::string raw_name( c_group_names.data() + i * MED_LNAME_SIZE, MED_LNAME_SIZE);
raw_name = StringUtilities::Trim(raw_name, true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain this change a bit? I thought the nullchars are already handled?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your comments Philipp.

With the original version, the group names contain garbage characters at the end. To fix this, I trim the strings at the "\0" null terminator.

At least on Ubuntu, this works.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I trim the strings at the "\0" null terminator.

StringUtilities::Trim should already be doing this. What is the failing input?
Could you please add the failing input in this test so that it could be fixed there?

// clean the name
auto pos = raw_name.find('\0');
if (pos != std::string::npos) {
raw_name = raw_name.substr(0, pos);
}
group_names[i] = raw_name;
}

groups_by_family[family_number] = std::move(group_names);
Expand Down Expand Up @@ -522,10 +529,33 @@ void MedModelPartIO::ReadModelPart(ModelPart& rThisModelPart)
num_nodes,
dimension);

// get global numbering for nodes, if the file contains them

@philbucher philbucher Feb 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah man, despite my best efforts the IDs are always finding their way back :D

Main reason I am against using ids is that they are often (ab) used to get nodes from ModelParts. This is a search, which is expensive for large models
=> one should use position in the ModelPart instead with is O(1)
Also non-consecutive IDs can create problems for MPI-partitioning

personal rant aside, this should be optional, as some meshers dont even write the IDs
I would vote to not read the IDs by default, but that decision I leave to current users or the tech-committee

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s true, but in any case, if we do nothing, the MED library enumerates them in its own way. This option is available in both Kratos and Salome, so for now, I think it is better to keep them synchronised. We can remove this later if needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well until now the Ids were just enuerated from the node position

In any case, I dont have a strong opinion

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's optional now, but locally just so we don't forget we're doing it until someone says something about it.

std::vector<med_int> node_ids(num_nodes);
med_err err = MEDmeshGlobalNumberRd(
mpFileHandler->GetFileHandle(),
mpFileHandler->GetMeshName(),
MED_NO_DT,
MED_NO_IT,
MED_NODE,
MED_NONE,
node_ids.data());

KRATOS_ERROR_IF(node_ids.empty()) << "MED file does not contain global numbering for nodes." << std::endl;
Comment thread
philbucher marked this conversation as resolved.
Outdated

if (err < 0) { // No global numbering = Use MED (1-based)
KRATOS_WARNING("MedModelPartIO")
<< "MED file does not contain global numbering for nodes. "
<< "Using MED implicit numbering." << std::endl;

for (med_int i = 0; i < num_nodes; ++i) {
node_ids[i] = i + 1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use std::iota or sth similar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
}

for (int i=0; i<num_nodes; ++i) {
std::array<double, 3> coords{0,0,0};
for (int j=0; j<dimension; ++j) {coords[j] = node_coords[i*dimension+j];}
IndexType new_node_id = i+1;
IndexType new_node_id = static_cast<IndexType>(node_ids[i]);

rThisModelPart.CreateNewNode(
new_node_id,
Expand Down Expand Up @@ -612,33 +642,79 @@ void MedModelPartIO::ReadModelPart(ModelPart& rThisModelPart)
connectivity.data());
CheckMEDErrorCode(err, "MEDmeshElementConnectivityRd");

// get global numbering for geometries, if the file contains them
std::vector<med_int> geom_global_ids(num_geometries);
const bool has_cell_global_numbering =
MEDmeshGlobalNumberRd(
mpFileHandler->GetFileHandle(),
mpFileHandler->GetMeshName(),
MED_NO_DT,
MED_NO_IT,
MED_CELL,
geo_type,
geom_global_ids.data()) >= 0;

// create geometries
const std::string kratos_geo_name = GetKratosGeometryName(geo_type, dimension);
const auto reorder_fct = GetReorderFunction<IndexType>(geo_type);

if (!has_cell_global_numbering) {
KRATOS_WARNING("MedModelPartIO")
<< "MED file does not contain global numbering for geometries of type "
<< kratos_geo_name << ". Using sequential numbering." << std::endl;
}

std::vector<IndexType> geom_node_ids(num_nodes_geo_type);

for (std::size_t i=0; i<static_cast<std::size_t>(num_geometries); ++i) {
for (int j=0; j<num_nodes_geo_type; ++j) {
const int node_idx = i*num_nodes_geo_type + j;
geom_node_ids[j] = connectivity[node_idx];
const med_int med_node_index = connectivity[node_idx]; // 1-based
KRATOS_ERROR_IF(med_node_index <= 0 || med_node_index > num_nodes)
<< "Invalid MED node index: " << med_node_index << std::endl;
geom_node_ids[j] = static_cast<IndexType>(
node_ids[med_node_index - 1]
);
}
reorder_fct(geom_node_ids);

// Avoid using nodes or points as geometries
if (geo_type == MED_POINT1) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are those skipped? Structural cases need them! (e.g. pointloads)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm skipping the creation of point geometries, as they don't represent real geometries in Kratos.

Here, node families are used to construct SubModelParts that contain only nodes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But they should represent real geometries. If you dont want them then you shouldnt create them in Salome.

See an example for a simple cantilever how they are used:
https://github.com/KratosMultiphysics/KratosSalomePlugin/blob/dce31197e15887b40174d27783ac2839b0134a2e/tui_examples/cantilever/salome_cantilever_hexa.py#L60

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (groups_by_fam.empty()) {continue;}
const int fam_num_node = geom_family_numbers[i];
if (fam_num_node == 0) {continue;}

const auto it_groups_node = groups_by_fam.find(fam_num_node);
KRATOS_ERROR_IF(it_groups_node == groups_by_fam.end()) << "Missing node family with number " << fam_num_node << "!" << std::endl;
for (const auto& r_smp_name_node : it_groups_node->second) {
smp_nodes[r_smp_name_node].insert(smp_nodes[r_smp_name_node].end(), geom_node_ids.begin(), geom_node_ids.end());
}
continue;
}

KRATOS_ERROR_IF(std::numeric_limits<decltype(num_geometries_total)>::max() == num_geometries_total)
<< "number of geometries read (" << num_geometries_total << ") exceeds the capacity of the index type";

IndexType kratos_geom_id;
// use global numbering (ids) for geometries, if the file contains them
if (has_cell_global_numbering) {
kratos_geom_id = static_cast<IndexType>(geom_global_ids[i]);
} else {
kratos_geom_id = ++num_geometries_total;
}

rThisModelPart.CreateNewGeometry(kratos_geo_name,
++num_geometries_total,
kratos_geom_id,
geom_node_ids);

if (groups_by_fam.empty()) {continue;} // file does not contain families
if (groups_by_fam.empty()) {continue;} // file does not contain fakratos_geom_idmilies
const int fam_num = geom_family_numbers[i];
if (fam_num == 0) {continue;} // geometry does not belong to a SubModelPart

const auto it_groups = groups_by_fam.find(fam_num);
KRATOS_ERROR_IF(it_groups == groups_by_fam.end()) << "Missing geometry family with number " << fam_num << "!" << std::endl;
for (const auto& r_smp_name : it_groups->second) {
smp_geoms[r_smp_name].push_back(num_geometries_total);
smp_geoms[r_smp_name].push_back(kratos_geom_id);
}

if (add_nodes_of_geometries) {
Expand All @@ -647,6 +723,9 @@ void MedModelPartIO::ReadModelPart(ModelPart& rThisModelPart)
smp_nodes[r_smp_name].insert(smp_nodes[r_smp_name].end(), geom_node_ids.begin(), geom_node_ids.end());
}
}
if (has_cell_global_numbering) {
++num_geometries_total;
}
}

KRATOS_INFO("MedModelPartIO") << "Read " << num_geometries << " geometries of type " << kratos_geo_name << std::endl;
Expand Down Expand Up @@ -684,7 +763,15 @@ void MedModelPartIO::WriteModelPart(const ModelPart& rThisModelPart)
// TODO use this?
// MEDfileCommentWr(mpFileHandler->GetFileHandle(), "A 2D unstructured mesh : 15 nodes, 12 cells");

constexpr med_int dimension = 3; // in Kratos, everything is 3D
// set working space dimension
med_int dimension = 0;
for (const auto& r_geom : rThisModelPart.Geometries()) {

dimension = std::max( dimension, static_cast<med_int>(r_geom.WorkingSpaceDimension()));
}
if (dimension == 0 || dimension == 1){
dimension = 3;
}
Comment on lines +777 to +780

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
med_int dimension = 0;
for (const auto& r_geom : rThisModelPart.Geometries()) {
dimension = std::max( dimension, static_cast<med_int>(r_geom.WorkingSpaceDimension()));
}
if (dimension == 0 || dimension == 1){
dimension = 3;
}
med_int dimension = 2;
for (const auto& r_geom : rThisModelPart.Geometries()) {
dimension = std::max( dimension, static_cast<med_int>(r_geom.WorkingSpaceDimension()));
}


med_err err = MEDmeshCr(
mpFileHandler->GetFileHandle(),
Expand Down Expand Up @@ -715,17 +802,88 @@ void MedModelPartIO::WriteModelPart(const ModelPart& rThisModelPart)
nodal_coords.data());
CheckMEDErrorCode(err, "MEDmeshNodeCoordinateWr");

if (rThisModelPart.NumberOfGeometries() == 0) {
return;
std::unordered_map<int, int> kratos_node_id_global_position;
std::unordered_map<int, int> med_node_id_global_position;
int pos = 0;
for (const auto& r_node : rThisModelPart.Nodes()) {
kratos_node_id_global_position[r_node.Id()] = pos;
med_node_id_global_position[r_node.Id()] = 1 + pos; // starting from 1, since salome cannot parse 0 as node ids
++pos;
}

// we deliberately do not care about IDs
std::unordered_map<int, int> kratos_id_to_med_id;
int med_id = 1; // starting from 1, since salome cannot parse 0 as node ids
std::vector<med_int> node_ids;
node_ids.reserve(rThisModelPart.NumberOfNodes());

for (const auto& r_node : rThisModelPart.Nodes()) {
kratos_id_to_med_id[r_node.Id()] = med_id++;
node_ids.push_back(static_cast<med_int>(r_node.Id()));
}

// save global numbering for nodes
err = MEDmeshGlobalNumberWr(
mpFileHandler->GetFileHandle(),
mpFileHandler->GetMeshName(),
MED_NO_DT,
MED_NO_IT,
MED_NODE,
MED_NONE,
static_cast<med_int>(rThisModelPart.NumberOfNodes()),
node_ids.data());

CheckMEDErrorCode(err, "MEDmeshGlobalNumberWr (nodes)");

med_int next_family = 1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI by definition node-families are positive, whereas geometry families are negative

std::unordered_map<std::string, med_int> smp_to_family;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remembered part of why I didnt implement the writing of SubModelParts: A family in med is NOT the same as a SubModelPart in Kratos. A family contains only one type of geometry (e.g. only Triangle3 or Tetra4).
The med equivalent of a SubModelPart is a group, which consists of multiple families. (actually the relation is the other way round, each family has groups)

Also what I think is not considered is if a geometry belongs to multiple SubModelParts.

Back then I thought the solution would be to tag each entity like is done for the MMG-stuff, but not sure


std::vector<const ModelPart*> all_sub_modelparts;

std::function<void(const ModelPart&)> collect_subparts =
[&](const ModelPart& mp) {
for (const auto& r_child : mp.SubModelParts()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

med doesnt know SubSubModelParts, I am curious how you handle it. Have you tested it?

all_sub_modelparts.push_back(&r_child);

smp_to_family[r_child.Name()] = -next_family++;
collect_subparts(r_child);
}
};

collect_subparts(rThisModelPart);
// set families from submodelparts
for (const auto& [name, fam_id] : smp_to_family) {

err = MEDfamilyCr(
mpFileHandler->GetFileHandle(),
mpFileHandler->GetMeshName(),
name.c_str(),
fam_id,
1,
name.c_str());

CheckMEDErrorCode(err, "MEDfamilyCr");
}

std::vector<med_int> node_family_numbers( rThisModelPart.NumberOfNodes(), 0);

for (const auto* r_smp : all_sub_modelparts) {
const auto it = smp_to_family.find(r_smp->Name());
if (it == smp_to_family.end()) continue;

if (r_smp->NumberOfNodes() > 0 && r_smp->NumberOfGeometries() == 0){
for (const auto& r_node : r_smp->Nodes()) {
node_family_numbers[kratos_node_id_global_position[r_node.Id()]] = it->second;
}
}
}
// set families for nodes
err = MEDmeshEntityFamilyNumberWr(
mpFileHandler->GetFileHandle(),
mpFileHandler->GetMeshName(),
MED_NO_DT, MED_NO_IT,
MED_NODE, MED_NONE,
node_family_numbers.size(),
node_family_numbers.data());

CheckMEDErrorCode(err, "MEDmeshEntityFamilyNumberWr (nodes)");

using ConnectivitiesType = std::vector<med_int>;
using ConnectivitiesVector = std::vector<ConnectivitiesType>;

Expand Down Expand Up @@ -776,7 +934,7 @@ void MedModelPartIO::WriteModelPart(const ModelPart& rThisModelPart)

ConnectivitiesType conn;
for (const auto& r_node : r_geom.Points()) {
conn.push_back(kratos_id_to_med_id[r_node.Id()]);
conn.push_back(med_node_id_global_position[r_node.Id()]);
}
conn_map[this_geom_type].push_back(conn);
np_map[this_geom_type] = r_geom.PointsNumber();
Expand All @@ -788,6 +946,61 @@ void MedModelPartIO::WriteModelPart(const ModelPart& rThisModelPart)
for (auto& [geom_type, conn] : conn_map) {
const auto med_geom_type = KratosToMedGeometryType.at(geom_type);
write_geometries(med_geom_type, np_map[geom_type], conn);

std::vector<med_int> geom_global_ids;
geom_global_ids.reserve(conn.size());

for (const auto& r_geom : rThisModelPart.Geometries()) {
if (r_geom.GetGeometryType() != geom_type) continue;

geom_global_ids.push_back(
static_cast<med_int>(r_geom.Id())
);
}

KRATOS_ERROR_IF(geom_global_ids.size() != conn.size())
<< "Mismatch between geometry count and global ID count" << std::endl;
// save global numbering for geometries
err = MEDmeshGlobalNumberWr(
mpFileHandler->GetFileHandle(),
mpFileHandler->GetMeshName(),
MED_NO_DT,
MED_NO_IT,
MED_CELL,
med_geom_type,
static_cast<med_int>(geom_global_ids.size()),
geom_global_ids.data());

CheckMEDErrorCode(err, "MEDmeshGlobalNumberWr (cells)");

const std::size_t nb_elem = conn.size();
std::vector<med_int> elem_family_numbers(nb_elem, 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::vector<med_int> elem_family_numbers(nb_elem, 0);
std::vector<med_int> geom_family_numbers(nb_elem, 0);


std::size_t local_idx = 0;
for (const auto& r_geom : rThisModelPart.Geometries()) {
if (r_geom.GetGeometryType() != geom_type) continue;

for (const auto* r_smp : all_sub_modelparts) {
const auto it = smp_to_family.find(r_smp->Name());
if (it == smp_to_family.end()) continue;

if (r_smp->HasGeometry(r_geom.Id())) {
elem_family_numbers[local_idx] = it->second;
break;
}
}
++local_idx;
}
// save family numbers for geometries
err = MEDmeshEntityFamilyNumberWr(
mpFileHandler->GetFileHandle(),
mpFileHandler->GetMeshName(),
MED_NO_DT, MED_NO_IT,
MED_CELL, med_geom_type,
elem_family_numbers.size(),
elem_family_numbers.data());

CheckMEDErrorCode(err, "MEDmeshEntityFamilyNumberWr (cells)");
}

KRATOS_INFO("MedModelPartIO") << "Writing file " << mFileName << " took " << timer << std::endl;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ void AddCustomUtilitiesToPython(pybind11::module& m)
py::arg("model_part_2"),
py::arg("check_sub_model_parts")=true)
.def_static("AddGeometriesFromElements", &MedTestingUtilities::AddGeometriesFromElements)
.def_static("AddGeometriesFromConditions", &MedTestingUtilities::AddGeometriesFromConditions)
.def_static("AddGeometriesFromElementsAndConditions", &MedTestingUtilities::AddGeometriesFromElementsAndConditions)
.def_static("AddGeometriesFromElementsAndConditionsAllLevels", &MedTestingUtilities::AddGeometriesFromElementsAndConditionsAllLevels)
.def_static("ComputeLength", &MedTestingUtilities::ComputeLength)
.def_static("ComputeArea", &MedTestingUtilities::ComputeArea)
.def_static("ComputeVolume", &MedTestingUtilities::ComputeVolume)
Expand Down
Loading
Loading