sgio 0.2.14__py3-none-any.whl → 0.3.0__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.
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ from functools import partial
4
5
 
5
6
  import numpy as np
7
+ from meshio import CellBlock, Mesh
6
8
 
7
9
 
8
10
  from ._common import (
@@ -16,11 +18,14 @@ from ._common import (
16
18
  _read_physical_names,
17
19
  _write_data,
18
20
  _write_physical_names,
21
+ num_nodes_per_cell,
22
+ cell_data_from_raw,
19
23
  )
20
24
  from sgio.iofunc._meshio import (
21
25
  warn,
22
26
  raw_from_cell_data,
23
- WriteError
27
+ WriteError,
28
+ ReadError,
24
29
  )
25
30
 
26
31
  logger = logging.getLogger(__name__)
@@ -29,6 +34,268 @@ c_int = np.dtype("i")
29
34
  c_size_t = np.dtype("P")
30
35
  c_double = np.dtype("d")
31
36
 
37
+
38
+ def _size_type(data_size):
39
+ return np.dtype(f"u{data_size}")
40
+
41
+
42
+ # ====================================================================
43
+ # Readers
44
+ # ====================================================================
45
+
46
+ def read_buffer(f, is_ascii: bool, data_size):
47
+ """Read gmsh 4.1 format mesh from buffer.
48
+
49
+ The format is specified at
50
+ <http://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format>.
51
+ """
52
+ # Initialize the optional data fields
53
+ points = []
54
+ cells = None
55
+ field_data = {}
56
+ cell_data_raw = {}
57
+ cell_tags = {}
58
+ point_data = {}
59
+ physical_tags = None
60
+ bounding_entities = None
61
+ cell_sets = {}
62
+ periodic = None
63
+
64
+ while True:
65
+ # fast-forward over blank lines
66
+ line, is_eof = _fast_forward_over_blank_lines(f)
67
+ if is_eof:
68
+ break
69
+
70
+ if line[0] != "$":
71
+ raise ReadError(f"Unexpected line {repr(line)}")
72
+
73
+ environ = line[1:].strip()
74
+
75
+ if environ == "PhysicalNames":
76
+ _read_physical_names(f, field_data)
77
+ elif environ == "Entities":
78
+ # Read physical tags and information on bounding entities.
79
+ physical_tags, bounding_entities = _read_entities(f, is_ascii, data_size)
80
+ elif environ == "Nodes":
81
+ points, point_tags, point_entities = _read_nodes(f, is_ascii, data_size)
82
+ elif environ == "Elements":
83
+ cells, cell_tags, cell_sets = _read_elements(
84
+ f,
85
+ point_tags,
86
+ physical_tags,
87
+ bounding_entities,
88
+ is_ascii,
89
+ data_size,
90
+ field_data,
91
+ )
92
+ elif environ == "Periodic":
93
+ periodic = _read_periodic(f, is_ascii, data_size)
94
+ elif environ == "NodeData":
95
+ _read_data(f, "NodeData", point_data, data_size, is_ascii)
96
+ elif environ == "ElementData":
97
+ _read_data(f, "ElementData", cell_data_raw, data_size, is_ascii)
98
+ else:
99
+ # Skip unrecognized sections
100
+ _fast_forward_to_end_block(f, environ)
101
+
102
+ if cells is None:
103
+ raise ReadError("$Element section not found.")
104
+
105
+ cell_data = cell_data_from_raw(cells, cell_data_raw)
106
+ cell_data.update(cell_tags)
107
+
108
+ # Add node entity information to the point data
109
+ point_data.update({"gmsh:dim_tags": point_entities})
110
+
111
+ return Mesh(
112
+ points,
113
+ cells,
114
+ point_data=point_data,
115
+ cell_data=cell_data,
116
+ field_data=field_data,
117
+ cell_sets=cell_sets,
118
+ gmsh_periodic=periodic,
119
+ )
120
+
121
+
122
+ def _read_entities(f, is_ascii: bool, data_size):
123
+ """Read the entity section.
124
+
125
+ Return physical tags of the entities, and (for entities of dimension > 0)
126
+ the bounding entities.
127
+ """
128
+ fromfile = partial(np.fromfile, sep=" " if is_ascii else "")
129
+ c_size_t = _size_type(data_size)
130
+ physical_tags = ({}, {}, {}, {})
131
+ bounding_entities = ({}, {}, {}, {})
132
+ number = fromfile(f, c_size_t, 4) # dims 0, 1, 2, 3
133
+
134
+ for d, n in enumerate(number):
135
+ for _ in range(n):
136
+ (tag,) = fromfile(f, c_int, 1)
137
+ fromfile(f, c_double, 3 if d == 0 else 6) # discard bounding-box
138
+ (num_physicals,) = fromfile(f, c_size_t, 1)
139
+ physical_tags[d][tag] = list(fromfile(f, c_int, num_physicals))
140
+ if d > 0:
141
+ # Number of bounding entities
142
+ num_BREP_ = fromfile(f, c_size_t, 1)[0]
143
+ # Store bounding entities
144
+ bounding_entities[d][tag] = fromfile(f, c_int, num_BREP_)
145
+
146
+ _fast_forward_to_end_block(f, "Entities")
147
+ return physical_tags, bounding_entities
148
+
149
+
150
+ def _read_nodes(f, is_ascii: bool, data_size):
151
+ """Read node data: Node coordinates and tags.
152
+
153
+ Also find the entities of the nodes, and store this as point_data.
154
+ Note that entity tags are 1-offset within each dimension, thus it is
155
+ necessary to keep track of both tag and dimension of the entity.
156
+ """
157
+ fromfile = partial(np.fromfile, sep=" " if is_ascii else "")
158
+ c_size_t = _size_type(data_size)
159
+
160
+ # numEntityBlocks numNodes minNodeTag maxNodeTag (all size_t)
161
+ num_entity_blocks, total_num_nodes, _, _ = fromfile(f, c_size_t, 4)
162
+
163
+ points = np.empty((total_num_nodes, 3), dtype=float)
164
+ tags = np.empty(total_num_nodes, dtype=int)
165
+ dim_tags = np.empty((total_num_nodes, 2), dtype=int)
166
+
167
+ idx = 0
168
+ for _ in range(num_entity_blocks):
169
+ # entityDim(int) entityTag(int) parametric(int) numNodes(size_t)
170
+ dim, entity_tag, parametric = fromfile(f, c_int, 3)
171
+ if parametric != 0:
172
+ raise ReadError("parametric nodes not implemented")
173
+ num_nodes = int(fromfile(f, c_size_t, 1)[0])
174
+
175
+ ixx = slice(idx, idx + num_nodes)
176
+ tags[ixx] = fromfile(f, c_size_t, num_nodes) - 1
177
+
178
+ # x(double) y(double) z(double) (* numNodes)
179
+ points[ixx] = fromfile(f, c_double, num_nodes * 3).reshape((num_nodes, 3))
180
+
181
+ # Entity tag and entity dimension of the nodes
182
+ dim_tags[ixx, 0] = dim
183
+ dim_tags[ixx, 1] = entity_tag
184
+ idx += num_nodes
185
+
186
+ _fast_forward_to_end_block(f, "Nodes")
187
+ return points, tags, dim_tags
188
+
189
+
190
+ def _read_elements(
191
+ f, point_tags, physical_tags, bounding_entities, is_ascii, data_size, field_data
192
+ ):
193
+ """Read element data from gmsh 4.1 file."""
194
+ fromfile = partial(np.fromfile, sep=" " if is_ascii else "")
195
+ c_size_t = _size_type(data_size)
196
+
197
+ # numEntityBlocks numElements minElementTag maxElementTag (all size_t)
198
+ num_entity_blocks, _, _, _ = fromfile(f, c_size_t, 4)
199
+
200
+ data = []
201
+ cell_data = {}
202
+ cell_sets = {k: [None] * num_entity_blocks for k in field_data.keys()}
203
+
204
+ for k in range(num_entity_blocks):
205
+ # entityDim(int) entityTag(int) elementType(int) numElements(size_t)
206
+ dim, tag, type_ele = fromfile(f, c_int, 3)
207
+ (num_ele,) = fromfile(f, c_size_t, 1)
208
+ for physical_name, cell_set in cell_sets.items():
209
+ cell_set[k] = np.arange(
210
+ num_ele
211
+ if (
212
+ physical_tags
213
+ and field_data[physical_name][1] == dim
214
+ and field_data[physical_name][0] in physical_tags[dim][tag]
215
+ )
216
+ else 0,
217
+ dtype=type(num_ele),
218
+ )
219
+ tpe = _gmsh_to_meshio_type[type_ele]
220
+ num_nodes_per_ele = num_nodes_per_cell[tpe]
221
+ d = fromfile(f, c_size_t, int(num_ele * (1 + num_nodes_per_ele))).reshape(
222
+ (num_ele, -1)
223
+ )
224
+
225
+ # Find physical tag, if defined; else it is None.
226
+ pt = None if not physical_tags else physical_tags[dim][tag]
227
+ # Bounding entities (of lower dimension) if defined.
228
+ if dim > 0 and bounding_entities:
229
+ be = bounding_entities[dim][tag]
230
+ else:
231
+ be = None
232
+ data.append((pt, be, tag, tpe, d))
233
+
234
+ _fast_forward_to_end_block(f, "Elements")
235
+
236
+ # Inverse point tags
237
+ inv_tags = np.full(np.max(point_tags) + 1, -1, dtype=int)
238
+ inv_tags[point_tags] = np.arange(len(point_tags))
239
+
240
+ # Note that the first column in the data array is the element tag; discard it.
241
+ # Ensure integer types for node indices
242
+ data = [
243
+ (physical_tag, bound_entity, geom_tag, tpe,
244
+ inv_tags[(d[:, 1:].astype(int) - 1)])
245
+ for physical_tag, bound_entity, geom_tag, tpe, d in data
246
+ ]
247
+
248
+ cells = []
249
+ for physical_tag, bound_entity, geom_tag, key, values in data:
250
+ # Ensure the cell data is integer type
251
+ cells.append(CellBlock(key, _gmsh_to_meshio_order(key, values.astype(int))))
252
+ if physical_tag:
253
+ if "gmsh:physical" not in cell_data:
254
+ cell_data["gmsh:physical"] = []
255
+ cell_data["gmsh:physical"].append(
256
+ np.full(len(values), physical_tag[0], int)
257
+ )
258
+ if "gmsh:geometrical" not in cell_data:
259
+ cell_data["gmsh:geometrical"] = []
260
+ cell_data["gmsh:geometrical"].append(np.full(len(values), geom_tag, int))
261
+
262
+ # The bounding entities is stored in the cell_sets.
263
+ if bounding_entities:
264
+ if "gmsh:bounding_entities" not in cell_sets:
265
+ cell_sets["gmsh:bounding_entities"] = []
266
+ cell_sets["gmsh:bounding_entities"].append(bound_entity)
267
+
268
+ return cells, cell_data, cell_sets
269
+
270
+
271
+ def _read_periodic(f, is_ascii, data_size):
272
+ """Read periodic information from gmsh 4.1 file."""
273
+ fromfile = partial(np.fromfile, sep=" " if is_ascii else "")
274
+ c_size_t = _size_type(data_size)
275
+ periodic = []
276
+ # numPeriodicLinks(size_t)
277
+ num_periodic = int(fromfile(f, c_size_t, 1)[0])
278
+ for _ in range(num_periodic):
279
+ # entityDim(int) entityTag(int) entityTagMaster(int)
280
+ edim, stag, mtag = fromfile(f, c_int, 3)
281
+ # numAffine(size_t) value(double) ...
282
+ num_affine = int(fromfile(f, c_size_t, 1)[0])
283
+ affine = fromfile(f, c_double, num_affine)
284
+ # numCorrespondingNodes(size_t)
285
+ num_nodes = int(fromfile(f, c_size_t, 1)[0])
286
+ # nodeTag(size_t) nodeTagMaster(size_t) ...
287
+ slave_master = fromfile(f, c_size_t, num_nodes * 2).reshape(-1, 2)
288
+ slave_master = slave_master - 1 # Subtract one, Python is 0-based
289
+ periodic.append([edim, (stag, mtag), affine, slave_master])
290
+
291
+ _fast_forward_to_end_block(f, "Periodic")
292
+ return periodic
293
+
294
+
295
+ # ====================================================================
296
+ # Writers
297
+ # ====================================================================
298
+
32
299
  def write_buffer(file, mesh, float_fmt, mesh_only, binary, **kwargs):
33
300
  """Writes msh files, cf.
34
301
  <http://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format>.
@@ -88,6 +355,10 @@ def write_buffer(file, mesh, float_fmt, mesh_only, binary, **kwargs):
88
355
  for name, dat in cell_data_raw.items():
89
356
  _write_data(file, "ElementData", name, dat, binary)
90
357
 
358
+ # Write cell_point_data (element nodal data) to ElementNodeData sections
359
+ if hasattr(mesh, 'cell_point_data') and mesh.cell_point_data:
360
+ _write_cell_point_data(file, mesh, binary)
361
+
91
362
 
92
363
  def _write_entities(fh, cells, tag_data, cell_sets, point_data, binary):
93
364
  """Write entity section in a .msh file.
@@ -531,3 +802,129 @@ def _write_periodic(fh, periodic, float_fmt: str, binary: bool) -> None:
531
802
  else:
532
803
  fh.write("\n")
533
804
  fh.write("$EndPeriodic\n")
805
+
806
+
807
+ def _write_cell_point_data(fh, mesh, binary: bool) -> None:
808
+ """Write cell_point_data (element nodal data) to $ElementNodeData sections.
809
+
810
+ Parameters
811
+ ----------
812
+ fh : file
813
+ File handle to write to
814
+ mesh : SGMesh
815
+ Mesh object containing cell_point_data
816
+ binary : bool
817
+ Whether to write in binary mode
818
+
819
+ Notes
820
+ -----
821
+ The $ElementNodeData format is:
822
+ $ElementNodeData
823
+ numStringTags(ASCII int)
824
+ stringTag(string) ...
825
+ numRealTags(ASCII int)
826
+ realTag(ASCII double) ...
827
+ numIntegerTags(ASCII int)
828
+ integerTag(ASCII int) ...
829
+ elementTag(int) numNodesPerElement(int) value(double) ...
830
+ ...
831
+ $EndElementNodeData
832
+
833
+ The cell_point_data structure is:
834
+ {name: [array_for_cell_block_0, array_for_cell_block_1, ...]}
835
+ where each array has shape (n_elements, n_nodes_per_element) for single component
836
+ or (n_elements, n_nodes_per_element, n_components) for multi-component data.
837
+ """
838
+ # Get element IDs from cell_data
839
+ if 'element_id' not in mesh.cell_data:
840
+ logger.warning("Cannot write cell_point_data: mesh.cell_data['element_id'] not found")
841
+ return
842
+
843
+ element_ids = mesh.cell_data['element_id']
844
+
845
+ # Process each field in cell_point_data
846
+ for field_name, cell_blocks_data in mesh.cell_point_data.items():
847
+ # Determine number of components from the data shape
848
+ first_block_data = cell_blocks_data[0]
849
+ if len(first_block_data.shape) == 2:
850
+ # Shape: (n_elements, n_nodes_per_element) - single component
851
+ num_components = 1
852
+ elif len(first_block_data.shape) == 3:
853
+ # Shape: (n_elements, n_nodes_per_element, n_components)
854
+ num_components = first_block_data.shape[2]
855
+ else:
856
+ logger.warning(f"Unexpected shape for cell_point_data '{field_name}': {first_block_data.shape}")
857
+ continue
858
+
859
+ # Count total number of elements across all cell blocks
860
+ total_elements = sum(len(block_data) for block_data in cell_blocks_data)
861
+
862
+ # Write header
863
+ if binary:
864
+ fh.write(b"$ElementNodeData\n")
865
+ # 1 string tag: field name
866
+ fh.write(f"{1}\n".encode())
867
+ fh.write(f'"{field_name}"\n'.encode())
868
+ # 1 real tag: time value
869
+ fh.write(f"{1}\n".encode())
870
+ fh.write(f"{0.0}\n".encode())
871
+ # 3 integer tags: time step, num components, num elements
872
+ fh.write(f"{3}\n".encode())
873
+ fh.write(f"{0}\n".encode()) # time step
874
+ fh.write(f"{num_components}\n".encode())
875
+ fh.write(f"{total_elements}\n".encode())
876
+ else:
877
+ fh.write("$ElementNodeData\n")
878
+ # 1 string tag: field name
879
+ fh.write(f"{1}\n")
880
+ fh.write(f'"{field_name}"\n')
881
+ # 1 real tag: time value
882
+ fh.write(f"{1}\n")
883
+ fh.write(f"{0.0}\n")
884
+ # 3 integer tags: time step, num components, num elements
885
+ fh.write(f"{3}\n")
886
+ fh.write(f"{0}\n") # time step
887
+ fh.write(f"{num_components}\n")
888
+ fh.write(f"{total_elements}\n")
889
+
890
+ # Write data for each element across all cell blocks
891
+ for block_idx, block_data in enumerate(cell_blocks_data):
892
+ block_element_ids = element_ids[block_idx]
893
+
894
+ for elem_idx, elem_data in enumerate(block_data):
895
+ elem_id = int(block_element_ids[elem_idx])
896
+
897
+ # elem_data shape: (n_nodes_per_element,) or (n_nodes_per_element, n_components)
898
+ if num_components == 1:
899
+ # Single component: elem_data is 1D array
900
+ num_nodes = len(elem_data)
901
+ if binary:
902
+ np.array([elem_id], dtype=c_int).tofile(fh)
903
+ np.array([num_nodes], dtype=c_int).tofile(fh)
904
+ elem_data.astype(c_double).tofile(fh)
905
+ else:
906
+ fh.write(f"{elem_id} {num_nodes}")
907
+ for val in elem_data:
908
+ fh.write(f" {float(val)}")
909
+ fh.write("\n")
910
+ else:
911
+ # Multi-component: elem_data is 2D array (n_nodes, n_components)
912
+ num_nodes = elem_data.shape[0]
913
+ if binary:
914
+ np.array([elem_id], dtype=c_int).tofile(fh)
915
+ np.array([num_nodes], dtype=c_int).tofile(fh)
916
+ # Flatten the data: all components for node 1, then node 2, etc.
917
+ elem_data.astype(c_double).flatten().tofile(fh)
918
+ else:
919
+ fh.write(f"{elem_id} {num_nodes}")
920
+ for node_vals in elem_data:
921
+ for val in node_vals:
922
+ fh.write(f" {float(val)}")
923
+ fh.write("\n")
924
+
925
+ # Write footer
926
+ if binary:
927
+ fh.write(b"\n")
928
+ fh.write(b"$EndElementNodeData\n")
929
+ else:
930
+ fh.write("$EndElementNodeData\n")