med2limit 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 simvia-tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: med2limit
3
+ Version: 0.0.1
4
+ Summary: Convert Code_Aster MED/RMED simulation files to LIMIT .linp / .lui input format
5
+ Author-email: Dorian Nezzar <dorian.nezzar@simvia.tech>
6
+ License-Expression: MIT
7
+ Keywords: med,rmed,code_aster,limit,fea,fatigue
8
+ Requires-Python: <=3.13,>=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: medcoupling>=9.15
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ <p align="center"><img src="logo/logo_med_coupling.png" alt="logo" width="50%"></p>
17
+
18
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
19
+
20
+ # med2limit
21
+ Convert Code_Aster MED/RMED simulation results into LIMIT `.linp` / `.lui` input files for fatigue analysis.
22
+
23
+ ## Features
24
+
25
+ - Shell workflows (DKT elements: S3, S4) with REPLO/CARCOQUE handling
26
+ - Linear solid workflows (C3D8 / HEXA8, C3D6 / PENTA6) with validated LIMIT node ordering
27
+ - Multi-step / multi-increment displacement and stress transfer
28
+ - Optional shell orientation file, or read directly from `IMPR_CONCEPT`-embedded result file
29
+ - Automatic detection of shell support level (works for shell-only and mixed hexa+shell models)
30
+
31
+ ## Code_aster Requirement
32
+ - Identify weld groups as Group_NO (not Group_MA), 1 node set per weld
33
+ - For shell element, extract top/bottom stresses as:
34
+ ```bash
35
+ SIEF_SUP=POST_CHAMP(RESULTAT=RESU,
36
+ EXTR_COQUE=_F(NOM_CHAM='SIEF_ELNO',
37
+ NUME_COUCHE=1,
38
+ NIVE_COUCHE='SUP',),);
39
+
40
+ SIEF_INF=POST_CHAMP(RESULTAT=RESU,
41
+ EXTR_COQUE=_F(NOM_CHAM='SIEF_ELNO',
42
+ NUME_COUCHE=1,
43
+ NIVE_COUCHE='INF',),);
44
+ ```
45
+ - For shell element, extract orientation/tichkness as:
46
+ ```bash
47
+ IMPR_CONCEPT(FORMAT='MED',
48
+ UNITE=80, --> Same unit as your results or in a dedicated file
49
+ CONCEPT=(_F(CARA_ELEM=Elem,
50
+ REPERE_LOCAL='ELEM',
51
+ MODELE=Modell,),),)
52
+ ```
53
+ ## Installation
54
+ Install med2limit with pip into a virtual python environnement (venv):
55
+
56
+ ```bash
57
+ python3 -m venv .venv
58
+ source .venv/bin/activate
59
+ pip install git+https://github.com/simvia-tech/med2limit.git@dev
60
+ ```
61
+
62
+
63
+ ## Usage
64
+
65
+ ### Command line
66
+ From 01_exemple in folder:
67
+ ```bash
68
+ med2limit/exemples/data
69
+ ```
70
+
71
+ ```bash
72
+ med2limit 01_exemple.rmed output.linp output.lui --groups "Shell1,Shell2" --nsets "WeldNo"
73
+ ```
74
+
75
+ With separate orientation file:
76
+
77
+ ```bash
78
+ med2limit 01_exemple.rmed output.linp output.lui 01_carcoc.rmed --groups "Shell1,Shell2" --nsets "WeldNo"
79
+ ```
80
+ # 01_exemple
81
+ <img src="./examples/images/01_exemple_LIMIT.png" width="50%">
82
+ Code_aster Shell-Shell geometry successfully imported in LIMIT Software
83
+
84
+ # 02_exemple
85
+ <img src="./examples/images/02_exemple_LIMIT.png" width="50%">
86
+ Code_aster Solid-Shell geometry successfully imported in LIMIT Software
87
+
88
+ ### Python API
89
+
90
+ ```python
91
+ from med2limit import MEDToLimitConverter
92
+
93
+ conv = MEDToLimitConverter(
94
+ med_filename="LIMIT1.rmed",
95
+ linp_filename="out.linp",
96
+ lui_filename="out.lui",
97
+ active_groups=["Shell1", "Shell2"],
98
+ active_nsets=["Weld"],
99
+ )
100
+ conv.convert()
101
+ ```
102
+
103
+ ## Package layout
104
+
105
+ ```
106
+ med2limit/
107
+ ├── element_types.py # MED↔LIMIT type mapping + helpers (pure)
108
+ ├── reader.py # MED file open + field lookup
109
+ ├── mesh.py # nodes, elements, GROUP_MA, GROUP_NO
110
+ ├── fields.py # DEPL + SIEF over all timesteps
111
+ ├── orientation.py # REPLO + CARCOQUE (embedded or separate)
112
+ ├── filter.py # active group selection + shell metadata mapping
113
+ ├── result_mapper.py # per-timestep stress/displacement mapping
114
+ ├── writer.py # .linp + .lui output
115
+ ├── converter.py # orchestrator (step_1 .. step_6 + convert)
116
+ └── cli.py # CLI + in-script config
117
+ ```
118
+
119
+ ## Testing
120
+
121
+ ```bash
122
+ pytest # all tests
123
+ pytest tests/test_element_types.py # one module
124
+ ```
125
+
126
+ ## Known limitations
127
+
128
+ - Quadratic solids (C3D10, C3D15, C3D20) — node ordering not yet validated in LIMIT
129
+ - Shell elsets with mixed thicknesses use the most-frequent value (with warning)
130
+
131
+ ## Acknowledgments
132
+
133
+ Special thanks to Tobias and Nikolaus for their feedback as early adopters
134
+ and their patience during the iterative development of the converter.
@@ -0,0 +1,119 @@
1
+ <p align="center"><img src="logo/logo_med_coupling.png" alt="logo" width="50%"></p>
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ # med2limit
6
+ Convert Code_Aster MED/RMED simulation results into LIMIT `.linp` / `.lui` input files for fatigue analysis.
7
+
8
+ ## Features
9
+
10
+ - Shell workflows (DKT elements: S3, S4) with REPLO/CARCOQUE handling
11
+ - Linear solid workflows (C3D8 / HEXA8, C3D6 / PENTA6) with validated LIMIT node ordering
12
+ - Multi-step / multi-increment displacement and stress transfer
13
+ - Optional shell orientation file, or read directly from `IMPR_CONCEPT`-embedded result file
14
+ - Automatic detection of shell support level (works for shell-only and mixed hexa+shell models)
15
+
16
+ ## Code_aster Requirement
17
+ - Identify weld groups as Group_NO (not Group_MA), 1 node set per weld
18
+ - For shell element, extract top/bottom stresses as:
19
+ ```bash
20
+ SIEF_SUP=POST_CHAMP(RESULTAT=RESU,
21
+ EXTR_COQUE=_F(NOM_CHAM='SIEF_ELNO',
22
+ NUME_COUCHE=1,
23
+ NIVE_COUCHE='SUP',),);
24
+
25
+ SIEF_INF=POST_CHAMP(RESULTAT=RESU,
26
+ EXTR_COQUE=_F(NOM_CHAM='SIEF_ELNO',
27
+ NUME_COUCHE=1,
28
+ NIVE_COUCHE='INF',),);
29
+ ```
30
+ - For shell element, extract orientation/tichkness as:
31
+ ```bash
32
+ IMPR_CONCEPT(FORMAT='MED',
33
+ UNITE=80, --> Same unit as your results or in a dedicated file
34
+ CONCEPT=(_F(CARA_ELEM=Elem,
35
+ REPERE_LOCAL='ELEM',
36
+ MODELE=Modell,),),)
37
+ ```
38
+ ## Installation
39
+ Install med2limit with pip into a virtual python environnement (venv):
40
+
41
+ ```bash
42
+ python3 -m venv .venv
43
+ source .venv/bin/activate
44
+ pip install git+https://github.com/simvia-tech/med2limit.git@dev
45
+ ```
46
+
47
+
48
+ ## Usage
49
+
50
+ ### Command line
51
+ From 01_exemple in folder:
52
+ ```bash
53
+ med2limit/exemples/data
54
+ ```
55
+
56
+ ```bash
57
+ med2limit 01_exemple.rmed output.linp output.lui --groups "Shell1,Shell2" --nsets "WeldNo"
58
+ ```
59
+
60
+ With separate orientation file:
61
+
62
+ ```bash
63
+ med2limit 01_exemple.rmed output.linp output.lui 01_carcoc.rmed --groups "Shell1,Shell2" --nsets "WeldNo"
64
+ ```
65
+ # 01_exemple
66
+ <img src="./examples/images/01_exemple_LIMIT.png" width="50%">
67
+ Code_aster Shell-Shell geometry successfully imported in LIMIT Software
68
+
69
+ # 02_exemple
70
+ <img src="./examples/images/02_exemple_LIMIT.png" width="50%">
71
+ Code_aster Solid-Shell geometry successfully imported in LIMIT Software
72
+
73
+ ### Python API
74
+
75
+ ```python
76
+ from med2limit import MEDToLimitConverter
77
+
78
+ conv = MEDToLimitConverter(
79
+ med_filename="LIMIT1.rmed",
80
+ linp_filename="out.linp",
81
+ lui_filename="out.lui",
82
+ active_groups=["Shell1", "Shell2"],
83
+ active_nsets=["Weld"],
84
+ )
85
+ conv.convert()
86
+ ```
87
+
88
+ ## Package layout
89
+
90
+ ```
91
+ med2limit/
92
+ ├── element_types.py # MED↔LIMIT type mapping + helpers (pure)
93
+ ├── reader.py # MED file open + field lookup
94
+ ├── mesh.py # nodes, elements, GROUP_MA, GROUP_NO
95
+ ├── fields.py # DEPL + SIEF over all timesteps
96
+ ├── orientation.py # REPLO + CARCOQUE (embedded or separate)
97
+ ├── filter.py # active group selection + shell metadata mapping
98
+ ├── result_mapper.py # per-timestep stress/displacement mapping
99
+ ├── writer.py # .linp + .lui output
100
+ ├── converter.py # orchestrator (step_1 .. step_6 + convert)
101
+ └── cli.py # CLI + in-script config
102
+ ```
103
+
104
+ ## Testing
105
+
106
+ ```bash
107
+ pytest # all tests
108
+ pytest tests/test_element_types.py # one module
109
+ ```
110
+
111
+ ## Known limitations
112
+
113
+ - Quadratic solids (C3D10, C3D15, C3D20) — node ordering not yet validated in LIMIT
114
+ - Shell elsets with mixed thicknesses use the most-frequent value (with warning)
115
+
116
+ ## Acknowledgments
117
+
118
+ Special thanks to Tobias and Nikolaus for their feedback as early adopters
119
+ and their patience during the iterative development of the converter.
@@ -0,0 +1,7 @@
1
+ """med2limit — MED/RMED to LIMIT converter for Code_Aster shell and solid models."""
2
+
3
+ from .converter import MEDToLimitConverter
4
+ from .cli import main
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["MEDToLimitConverter", "main"]
@@ -0,0 +1,106 @@
1
+ """
2
+ Command-line entry point.
3
+
4
+ Two modes:
5
+ - in-script configuration: edit the constants below for Salome / IDE direct runs
6
+ - CLI: standard argparse mode
7
+
8
+ This module does NOT call sys.exit() so it stays Salome-safe.
9
+ """
10
+
11
+ import argparse
12
+ import os
13
+
14
+ from .converter import MEDToLimitConverter
15
+
16
+
17
+ VERSION = "0.1.0"
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # In-script configuration (edit for Salome / direct IDE execution)
22
+ # ---------------------------------------------------------------------------
23
+
24
+ INPUT_MED = ""
25
+ OUTPUT_LINP = ""
26
+ OUTPUT_LUI = ""
27
+ ORIENTATION_MED = None
28
+ ACTIVE_GROUPS = []
29
+ ACTIVE_NSETS = []
30
+
31
+ USE_IN_SCRIPT_CONFIGURATION = False
32
+
33
+
34
+ def _parse_name_list(text_value: str):
35
+ if not text_value:
36
+ return []
37
+ return [item.strip() for item in text_value.split(",") if item.strip()]
38
+
39
+
40
+ def _from_in_script_config():
41
+ return (
42
+ INPUT_MED,
43
+ OUTPUT_LINP or os.path.splitext(INPUT_MED)[0] + ".linp",
44
+ OUTPUT_LUI or os.path.splitext(INPUT_MED)[0] + ".lui",
45
+ ORIENTATION_MED,
46
+ list(ACTIVE_GROUPS),
47
+ list(ACTIVE_NSETS),
48
+ )
49
+
50
+
51
+ def _from_cli():
52
+ parser = argparse.ArgumentParser(
53
+ description=f"MED/RMED to LIMIT converter ({VERSION})"
54
+ )
55
+ parser.add_argument("input_med")
56
+ parser.add_argument("output_linp", nargs="?")
57
+ parser.add_argument("output_lui", nargs="?")
58
+ parser.add_argument("orientation_med", nargs="?", default=None)
59
+ parser.add_argument("--groups", default="")
60
+ parser.add_argument("--nsets", default="")
61
+ args, _unknown = parser.parse_known_args()
62
+
63
+ return (
64
+ args.input_med,
65
+ args.output_linp or os.path.splitext(args.input_med)[0] + ".linp",
66
+ args.output_lui or os.path.splitext(args.input_med)[0] + ".lui",
67
+ args.orientation_med,
68
+ _parse_name_list(args.groups),
69
+ _parse_name_list(args.nsets),
70
+ )
71
+
72
+
73
+ def main():
74
+ if USE_IN_SCRIPT_CONFIGURATION and INPUT_MED:
75
+ (input_file, out_linp, out_lui, orient, groups, nsets) = _from_in_script_config()
76
+ print("Running in in-script configuration mode")
77
+ else:
78
+ (input_file, out_linp, out_lui, orient, groups, nsets) = _from_cli()
79
+
80
+ print(f" input : {input_file}")
81
+ print(f" output_linp : {out_linp}")
82
+ print(f" output_lui : {out_lui}")
83
+ print(f" orientation_med: {orient}")
84
+ print(f" groups : {groups}")
85
+ print(f" nsets : {nsets}")
86
+
87
+ if not os.path.exists(input_file):
88
+ print(f"ERROR: Input file not found: {input_file}")
89
+ return 1
90
+ if orient and not os.path.exists(orient):
91
+ print(f"ERROR: Orientation file not found: {orient}")
92
+ return 1
93
+
94
+ converter = MEDToLimitConverter(
95
+ med_filename=input_file,
96
+ linp_filename=out_linp,
97
+ lui_filename=out_lui,
98
+ orientation_med_filename=orient,
99
+ active_groups=groups,
100
+ active_nsets=nsets,
101
+ )
102
+ return 0 if converter.convert() else 1
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
@@ -0,0 +1,127 @@
1
+ """
2
+ Top-level orchestrator. Public step methods allow debugging in a notebook.
3
+
4
+ Typical use:
5
+ converter = MEDToLimitConverter("model.rmed", "out.linp", "out.lui",
6
+ active_groups=["Shell1", "Shell2"])
7
+ converter.convert()
8
+
9
+ Step-by-step debug:
10
+ converter.step_1_load()
11
+ converter.step_2_extract_mesh()
12
+ print(converter.mesh.all_nodes) # inspect
13
+ converter.step_3_extract_fields()
14
+ ...
15
+ """
16
+
17
+ from .reader import MedFileReader
18
+ from .mesh import MeshExtractor
19
+ from .fields import FieldExtractor
20
+ from .orientation import ShellMetadata
21
+ from .filter import ActiveFilter
22
+ from .writer import LinpWriter, LuiWriter
23
+
24
+
25
+ class MEDToLimitConverter:
26
+ """Orchestrate the MED → LIMIT conversion pipeline."""
27
+
28
+ def __init__(self, med_filename, linp_filename, lui_filename,
29
+ orientation_med_filename=None,
30
+ active_groups=None, active_nsets=None):
31
+ self.med_filename = med_filename
32
+ self.linp_filename = linp_filename
33
+ self.lui_filename = lui_filename
34
+ self.orientation_med_filename = orientation_med_filename
35
+ self.active_groups = list(active_groups or [])
36
+ self.active_nsets = list(active_nsets or [])
37
+
38
+ # Filled at runtime
39
+ self.reader = None
40
+ self.mesh = None
41
+ self.fields = None
42
+ self.shell_meta = None
43
+ self.filter = None
44
+
45
+ # --------------------------------------------------------------- steps
46
+
47
+ def step_1_load(self):
48
+ """Open the main MED/RMED file."""
49
+ print(f"Loading MED file: {self.med_filename}")
50
+ self.reader = MedFileReader(self.med_filename)
51
+ print(f" Found {len(self.reader.meshes)} mesh(es)")
52
+ print(f" Found {len(self.reader.fields)} field(s)")
53
+ for f in self.reader.fields:
54
+ print(f" - {f.getName()}")
55
+
56
+ def step_2_extract_mesh(self):
57
+ """Extract nodes, elements, element groups and node groups."""
58
+ print("\nExtracting mesh...")
59
+ self.mesh = MeshExtractor(
60
+ self.reader.meshes, self.active_groups, self.active_nsets
61
+ )
62
+ self.mesh.extract_all()
63
+ print(f" Total nodes: {len(self.mesh.all_nodes)}")
64
+ print(f" Total elements: {len(self.mesh.all_elements)}")
65
+ print(f" Element sets: {list(self.mesh.element_sets.keys())}")
66
+ print(f" Node sets: {list(self.mesh.node_sets.keys())}")
67
+
68
+ def step_3_extract_fields(self):
69
+ """Extract DEPL and SIEF fields for all time steps."""
70
+ print("\nExtracting fields...")
71
+ self.fields = FieldExtractor(self.reader)
72
+ self.fields.extract()
73
+ print(f" Stress mode: {self.fields.stress_mode}")
74
+ print(f" Time steps: {self.fields.n_timesteps}")
75
+
76
+ def step_4_load_shell_metadata(self):
77
+ """Load REPLO/CARCOQUE: embedded first, then separate file as fallback."""
78
+ print("\nLoading shell metadata (REPLO, CARCOQUE)...")
79
+ self.shell_meta = ShellMetadata()
80
+ loaded = self.shell_meta.load(self.reader, self.orientation_med_filename)
81
+ if loaded:
82
+ print(f" Loaded from: {self.shell_meta.source}")
83
+ if self.shell_meta.replo1 is not None:
84
+ print(f" REPLO_1: {len(self.shell_meta.replo1)} entries")
85
+ if self.shell_meta.carcoque_ep is not None:
86
+ uniq = sorted({round(float(v), 3) for v in self.shell_meta.carcoque_ep})
87
+ print(f" CARCOQUE EP unique values: {uniq}")
88
+ else:
89
+ print(" No shell metadata available (using defaults)")
90
+
91
+ def step_5_filter(self):
92
+ """Reduce model to active groups and map shell metadata."""
93
+ print("\nFiltering active data...")
94
+ self.filter = ActiveFilter(
95
+ self.mesh, self.shell_meta,
96
+ requested_groups=self.active_groups,
97
+ requested_nsets=self.active_nsets,
98
+ )
99
+ self.filter.apply()
100
+ print(f" Active elements: {len(self.filter.active_elem_ids)}")
101
+ print(f" Active nodes: {len(self.filter.active_node_ids)}")
102
+
103
+ def step_6_write(self):
104
+ """Write the .linp and .lui output files."""
105
+ LinpWriter(self.mesh, self.filter, self.med_filename).write(self.linp_filename)
106
+ LuiWriter(self.mesh, self.filter, self.fields, self.med_filename).write(self.lui_filename)
107
+
108
+ # --------------------------------------------------------------- full
109
+
110
+ def convert(self):
111
+ """Run the complete pipeline."""
112
+ try:
113
+ self.step_1_load()
114
+ self.step_2_extract_mesh()
115
+ self.step_3_extract_fields()
116
+ self.step_4_load_shell_metadata()
117
+ self.step_5_filter()
118
+ self.step_6_write()
119
+ print("\n" + "=" * 60)
120
+ print("Translation complete")
121
+ print("=" * 60)
122
+ return True
123
+ except Exception as e:
124
+ import traceback
125
+ print(f"\nERROR: Conversion failed: {e}")
126
+ print(traceback.format_exc())
127
+ return False
@@ -0,0 +1,80 @@
1
+ """
2
+ MED ↔ LIMIT element type mapping and classification helpers.
3
+
4
+ This module is pure: no I/O, no MEDCoupling reads. Only mapping tables and
5
+ small functions. Easy to test in isolation.
6
+ """
7
+
8
+ import medcoupling as mc
9
+
10
+
11
+ # Mapping from MEDCoupling geometric type codes to LIMIT/Abaqus element type names.
12
+ MED_TO_LIMIT = {
13
+ # Solids
14
+ mc.NORM_HEXA8: "C3D8",
15
+ mc.NORM_HEXA20: "C3D20",
16
+ mc.NORM_TETRA4: "C3D4",
17
+ mc.NORM_TETRA10: "C3D10",
18
+ mc.NORM_PENTA6: "C3D6",
19
+ mc.NORM_PENTA15: "C3D15",
20
+ # Shells / 2D
21
+ mc.NORM_TRI3: "S3",
22
+ mc.NORM_QUAD4: "S4",
23
+ mc.NORM_TRI6: "STRI65",
24
+ mc.NORM_QUAD8: "S8R",
25
+ # Beams / 1D
26
+ mc.NORM_SEG2: "T3D2",
27
+ mc.NORM_SEG3: "B32",
28
+ }
29
+
30
+
31
+ # Validated MED → LIMIT local-node permutations for linear 3D solids.
32
+ # Identified by direct LIMIT_CAE geometry tests.
33
+ NODE_REORDER = {
34
+ "C3D8": [0, 3, 2, 1, 4, 7, 6, 5],
35
+ "C3D6": [0, 2, 1, 3, 5, 4],
36
+ }
37
+
38
+
39
+ def med_to_limit(geo_type):
40
+ """Return the LIMIT name for a MEDCoupling geometric type, or a placeholder."""
41
+ return MED_TO_LIMIT.get(geo_type, f"UNKNOWN_{geo_type}")
42
+
43
+
44
+ def is_shell(elem_type: str) -> bool:
45
+ """True for shell-like element types handled by this tool."""
46
+ return elem_type.startswith("S") or elem_type.startswith("M")
47
+
48
+
49
+ def is_solid(elem_type: str) -> bool:
50
+ """True for 3D solid element types."""
51
+ return elem_type.startswith("C3D")
52
+
53
+
54
+ def is_beam_or_truss(elem_type: str) -> bool:
55
+ """True for 1D beam/truss element types."""
56
+ return elem_type.startswith("T3D") or elem_type.startswith("B")
57
+
58
+
59
+ def is_result_carrying(elem_type: str) -> bool:
60
+ """True for element types that carry stress/displacement results in LIMIT."""
61
+ return is_shell(elem_type) or is_solid(elem_type)
62
+
63
+
64
+ def get_reorder_indices(elem_type: str, n_nodes: int):
65
+ """Return the validated MED→LIMIT local-node permutation, or identity."""
66
+ perm = NODE_REORDER.get(elem_type)
67
+ if perm is None or len(perm) != n_nodes:
68
+ return list(range(n_nodes))
69
+ return perm
70
+
71
+
72
+ def reorder_connectivity(elem_type: str, connectivity):
73
+ """Reorder a connectivity list with the validated MED→LIMIT permutation."""
74
+ order = get_reorder_indices(elem_type, len(connectivity))
75
+ return [connectivity[i] for i in order]
76
+
77
+
78
+ def clean_name(name) -> str:
79
+ """Normalize a group name by removing underscores (LIMIT naming convention)."""
80
+ return str(name).replace("_", "")