med2limit 0.0.1__py3-none-any.whl

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.
med2limit/__init__.py ADDED
@@ -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"]
med2limit/cli.py ADDED
@@ -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()
med2limit/converter.py ADDED
@@ -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("_", "")
med2limit/fields.py ADDED
@@ -0,0 +1,108 @@
1
+ """
2
+ Field extraction: displacement (DEPL) and stress (SIEF).
3
+
4
+ Two stress modes are detected automatically:
5
+ - 'shell_top_bottom' — SIEF_INF + SIEF_SUP (shell top/bottom faces)
6
+ - 'generic' — SIEF_ELNO (solid or generic ELNO stress)
7
+
8
+ All time steps are extracted at once. Per-step mapping to per-element data
9
+ happens later in writer.py / filter.py.
10
+ """
11
+
12
+
13
+ class FieldExtractor:
14
+ """Extract raw displacement and stress arrays for every time step.
15
+
16
+ Outputs after `extract()`:
17
+ - n_timesteps: int
18
+ - stress_mode: 'shell_top_bottom' | 'generic' | None
19
+ - disp_raw_ts: list of ndarray (one per timestep)
20
+ - stress_inf_raw_ts: list of ndarray | None (only if shell mode)
21
+ - stress_sup_raw_ts: list of ndarray | None (only if shell mode)
22
+ - stress_generic_raw_ts: list of ndarray | None (only if generic mode)
23
+ """
24
+
25
+ def __init__(self, reader):
26
+ self.reader = reader
27
+ self.n_timesteps = 0
28
+ self.stress_mode = None
29
+ self.disp_raw_ts = []
30
+ self.stress_inf_raw_ts = []
31
+ self.stress_sup_raw_ts = []
32
+ self.stress_generic_raw_ts = []
33
+
34
+ def extract(self):
35
+ disp_field = self._find_displacement_field()
36
+ if disp_field is None:
37
+ raise RuntimeError("No displacement field found (DEPL). Cannot proceed.")
38
+
39
+ stress_inf_field = self.reader.find_field("SIEF_INFSIEF_ELNO")
40
+ stress_sup_field = self.reader.find_field("SIEF_SUPSIEF_ELNO")
41
+ stress_generic_field = self.reader.find_field(
42
+ "SIEF_ELNO", exclude=("INF", "SUP")
43
+ )
44
+
45
+ # Track what's available — they're not mutually exclusive anymore
46
+ self.has_shell_stress = (stress_inf_field is not None and stress_sup_field is not None)
47
+ self.has_generic_stress = stress_generic_field is not None
48
+
49
+ # Legacy flag for backward compat (e.g. logging)
50
+ if self.has_shell_stress and self.has_generic_stress:
51
+ self.stress_mode = "mixed"
52
+ elif self.has_shell_stress:
53
+ self.stress_mode = "shell_top_bottom"
54
+ elif self.has_generic_stress:
55
+ self.stress_mode = "generic"
56
+ else:
57
+ self.stress_mode = None
58
+
59
+ self.n_timesteps = disp_field.getNumberOfTS()
60
+ self.disp_raw_ts = [None] * self.n_timesteps
61
+ self.stress_inf_raw_ts = [None] * self.n_timesteps
62
+ self.stress_sup_raw_ts = [None] * self.n_timesteps
63
+ self.stress_generic_raw_ts = [None] * self.n_timesteps
64
+
65
+ for it in range(self.n_timesteps):
66
+ self._extract_one_step(
67
+ it, disp_field, stress_inf_field, stress_sup_field, stress_generic_field
68
+ )
69
+
70
+ def _find_displacement_field(self):
71
+ """Find a field whose name contains DEPL (e.g., RESU____DEPL)."""
72
+ for field in self.reader.fields:
73
+ if "DEPL" in field.getName():
74
+ return field
75
+ return None
76
+
77
+ def _extract_one_step(self, it, disp_field, inf_field, sup_field, gen_field):
78
+ # Displacement
79
+ try:
80
+ self.disp_raw_ts[it] = (
81
+ disp_field.getTimeStepAtPos(it).getUndergroundDataArray().toNumPyArray()
82
+ )
83
+ except Exception as e:
84
+ print(f" WARNING: TS={it}: could not extract displacement: {e}")
85
+
86
+ # Generic stress (for solids) — independent of shell stress
87
+ if self.has_generic_stress:
88
+ try:
89
+ self.stress_generic_raw_ts[it] = (
90
+ gen_field.getTimeStepAtPos(it).getUndergroundDataArray().toNumPyArray()
91
+ )
92
+ except Exception as e:
93
+ print(f" WARNING: TS={it}: could not extract generic stress: {e}")
94
+
95
+ # Shell top/bottom (for shells) — independent of generic stress
96
+ if self.has_shell_stress:
97
+ try:
98
+ self.stress_inf_raw_ts[it] = (
99
+ inf_field.getTimeStepAtPos(it).getUndergroundDataArray().toNumPyArray()
100
+ )
101
+ except Exception as e:
102
+ print(f" WARNING: TS={it}: could not extract stress_inf: {e}")
103
+ try:
104
+ self.stress_sup_raw_ts[it] = (
105
+ sup_field.getTimeStepAtPos(it).getUndergroundDataArray().toNumPyArray()
106
+ )
107
+ except Exception as e:
108
+ print(f" WARNING: TS={it}: could not extract stress_sup: {e}")
med2limit/filter.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Active filter: select element groups to export and trim everything else.
3
+
4
+ Also applies shell metadata (orientations and thickness) to the active
5
+ shell elements by geometric signature lookup.
6
+ """
7
+
8
+ from .element_types import is_shell, is_solid
9
+ from .orientation import build_active_shell_signatures
10
+
11
+
12
+ class ActiveFilter:
13
+ """Reduce the model to a chosen subset of element groups.
14
+
15
+ After `apply()`:
16
+ - mesh.all_elements / all_nodes / element_sets / node_sets are filtered in place
17
+ - active_elem_ids: set[int]
18
+ - active_node_ids: set[int]
19
+ - shell_orientations: {elem_id: (D1, D2)} (only mapped shells)
20
+ - shell_thickness: {elem_id: float} (only mapped shells)
21
+ - has_shell_elements / has_solid_elements: bool
22
+ """
23
+
24
+ def __init__(self, mesh, shell_metadata, requested_groups=None, requested_nsets=None):
25
+ self.mesh = mesh
26
+ self.shell_metadata = shell_metadata
27
+ self.requested_groups = set(requested_groups or [])
28
+ self.requested_nsets = set(requested_nsets or [])
29
+
30
+ self.active_elem_ids = set()
31
+ self.active_node_ids = set()
32
+ self.shell_orientations = {}
33
+ self.shell_thickness = {}
34
+ self.has_shell_elements = False
35
+ self.has_solid_elements = False
36
+
37
+ # ------------------------------------------------------------------ public
38
+
39
+ def apply(self):
40
+ selected = self._select_group_names()
41
+ self._trim_elements_and_nodes(selected)
42
+ self._trim_node_sets()
43
+ self._compute_capability_flags()
44
+ self._map_shell_metadata()
45
+
46
+ # ----------------------------------------------------------------- private
47
+
48
+ def _select_group_names(self):
49
+ """Pick the set of element-group names to keep.
50
+
51
+ Priority:
52
+ 1. Explicit requested_groups (if any names match)
53
+ 2. User-defined GROUP_MA names extracted by the mesh
54
+ 3. All solid-only sets if the model contains solids
55
+ 4. Otherwise all shell sets
56
+ """
57
+ es = self.mesh.element_sets
58
+
59
+ if self.requested_groups:
60
+ selected = {name for name in es if name in self.requested_groups}
61
+ if selected:
62
+ return selected
63
+ print(" WARNING: requested active groups not found — falling back to auto selection")
64
+
65
+ if self.mesh.user_element_group_names:
66
+ return set(self.mesh.user_element_group_names)
67
+
68
+ all_types = {e["type"] for e in self.mesh.all_elements.values()}
69
+ if any(is_solid(t) for t in all_types):
70
+ return {name for name, d in es.items() if is_solid(d["type"])}
71
+ return {name for name, d in es.items() if is_shell(d["type"])}
72
+
73
+ def _trim_elements_and_nodes(self, selected_names):
74
+ active_elem_ids = set()
75
+ kept_sets = {}
76
+ for name, data in self.mesh.element_sets.items():
77
+ if name not in selected_names:
78
+ continue
79
+ ids = [eid for eid in data["elements"] if eid in self.mesh.all_elements]
80
+ if not ids:
81
+ continue
82
+ kept_sets[name] = {"type": data["type"], "elements": ids}
83
+ active_elem_ids.update(ids)
84
+
85
+ if not active_elem_ids:
86
+ raise RuntimeError(
87
+ "No active elements selected. Check element group names or model contents."
88
+ )
89
+
90
+ self.mesh.element_sets = kept_sets
91
+ self.mesh.all_elements = {
92
+ eid: d for eid, d in self.mesh.all_elements.items() if eid in active_elem_ids
93
+ }
94
+
95
+ active_node_ids = set()
96
+ for elem in self.mesh.all_elements.values():
97
+ active_node_ids.update(elem["connectivity"])
98
+ self.mesh.all_nodes = {
99
+ nid: c for nid, c in self.mesh.all_nodes.items() if nid in active_node_ids
100
+ }
101
+
102
+ self.active_elem_ids = active_elem_ids
103
+ self.active_node_ids = active_node_ids
104
+
105
+ def _trim_node_sets(self):
106
+ if self.requested_nsets:
107
+ self.mesh.node_sets = {
108
+ name: ids for name, ids in self.mesh.node_sets.items()
109
+ if name in self.requested_nsets
110
+ }
111
+ else:
112
+ # Keep only nodes that survived the element filter
113
+ self.mesh.node_sets = {
114
+ name: [nid for nid in ids if nid in self.active_node_ids]
115
+ for name, ids in self.mesh.node_sets.items()
116
+ }
117
+ self.mesh.node_sets = {n: ids for n, ids in self.mesh.node_sets.items() if ids}
118
+
119
+ def _compute_capability_flags(self):
120
+ types = {d["type"] for d in self.mesh.all_elements.values()}
121
+ self.has_shell_elements = any(is_shell(t) for t in types)
122
+ self.has_solid_elements = any(is_solid(t) for t in types)
123
+
124
+ def _map_shell_metadata(self):
125
+ if not self.has_shell_elements or self.shell_metadata is None:
126
+ return
127
+
128
+ sigs = build_active_shell_signatures(self.mesh.all_nodes, self.mesh.all_elements)
129
+ if not sigs:
130
+ return
131
+
132
+ n_orient = 0
133
+ n_thick = 0
134
+ for elem_id, _sig in sigs.items():
135
+ ori = self.shell_metadata.get_orientation(elem_id, sigs)
136
+ if ori is not None:
137
+ self.shell_orientations[elem_id] = ori
138
+ n_orient += 1
139
+ thick = self.shell_metadata.get_thickness(elem_id, sigs)
140
+ if thick is not None:
141
+ self.shell_thickness[elem_id] = thick
142
+ n_thick += 1
143
+
144
+ print(f" Mapped {n_orient} shell orientations and {n_thick} shell thicknesses")