11"""
22I/O for Nastran bulk data.
33"""
4+ from __future__ import annotations
5+
46import numpy as np
57
68from ..__about__ import __version__
@@ -63,18 +65,22 @@ def read_buffer(f):
6365 cells = []
6466 cells_id = []
6567 cell = None
66- cell_type = None
6768 point_refs = []
6869 cell_refs = []
6970 cell_ref = None
7071
71- def add_cell (nastran_type , cell , cell_type , cell_ref ):
72- cell_type = nastran_to_meshio_type [keyword ]
72+ def add_cell (nastran_type , cell , cell_ref ):
73+ cell_type = nastran_to_meshio_type [nastran_type ]
7374 cell = list (map (int , cell ))
7475
7576 # Treat 2nd order CTETRA, CPYRA, CPENTA, CHEXA elements
7677 if len (cell ) > num_nodes_per_cell [cell_type ]:
77- assert cell_type in ["tetra" , "pyramid" , "wedge" , "hexahedron" ]
78+ assert cell_type in [
79+ "tetra" ,
80+ "pyramid" ,
81+ "wedge" ,
82+ "hexahedron" ,
83+ ], f"Illegal cell type { cell_type } "
7884 if cell_type == "tetra" :
7985 cell_type = "tetra10"
8086 nastran_type = "CTETRA_"
@@ -90,6 +96,7 @@ def add_cell(nastran_type, cell, cell_type, cell_ref):
9096
9197 cell = _convert_to_vtk_ordering (cell , nastran_type )
9298
99+ # decide if we should append cell or start a new cell block
93100 if len (cells ) > 0 and cells [- 1 ][0 ] == cell_type :
94101 cells [- 1 ][1 ].append (cell )
95102 cells_id [- 1 ].append (cell_id )
@@ -114,48 +121,106 @@ def add_cell(nastran_type, cell, cell_type, cell_ref):
114121 if next_line .startswith ("ENDDATA" ):
115122 break
116123
117- # read line and merge with all continuation lines (starting with `+`)
118- chunks = _chunk_line (next_line )
124+ # read line and merge with all continuation lines (starting with `+` or
125+ # `*` or automatic continuation lines in fixed format)
126+ chunks = []
127+ c , _ = _chunk_line (next_line )
128+ chunks .append (c )
119129 while True :
120130 next_line = f .readline ()
131+
121132 if not next_line :
122133 raise ReadError ("Premature EOF" )
123- next_line = next_line . rstrip ()
134+
124135 # Blank lines or comments
125136 if len (next_line ) < 4 or next_line .startswith (("$" , "//" , "#" )):
126137 continue
127- elif next_line [0 ] == "+" :
128- # skip the continuation chunk
129- chunks += _chunk_line (next_line )[1 :]
138+
139+ elif next_line [0 ] in ["+" , "*" ]:
140+ # From
141+ # <https://docs.plm.automation.siemens.com/data_services/resources/nxnastran/10/help/en_US/tdocExt/pdf/User.pdf>:
142+ # You can manually specify a continuation by using a
143+ # continuation identifier. A continuation identifier is a
144+ # special character (+ or *) that indicates that the data
145+ # continues on another line.
146+ assert len (chunks [- 1 ]) <= 10
147+ if len (chunks [- 1 ]) == 10 :
148+ # This is a continuation line, so the 10th chunk of the
149+ # previous line must also be a continuation indicator.
150+ # Sometimes its first character is a `+`, but it's not
151+ # always present. Anyway, cut it off.
152+ chunks [- 1 ][- 1 ] = None
153+ c , _ = _chunk_line (next_line )
154+ c [0 ] = None
155+ chunks .append (c )
156+
157+ elif len (chunks [- 1 ]) == 10 and chunks [- 1 ][- 1 ] == " " :
158+ # automatic continuation: last chunk of previous line and first
159+ # chunk of current line are spaces
160+ c , _ = _chunk_line (next_line )
161+ if c [0 ] == " " :
162+ chunks [- 1 ][9 ] = None
163+ c [0 ] = None
164+ chunks .append (c )
165+ else :
166+ # not a continuation
167+ break
130168 else :
131169 break
132170
171+ # merge chunks according to large field format
172+ # large field format: 8 + 16 + 16 + 16 + 16 + 8
173+ if chunks [0 ][0 ].startswith ("GRID*" ):
174+ new_chunks = []
175+ for c in chunks :
176+ d = [c [0 ]]
177+
178+ if len (c ) > 1 :
179+ d .append (c [1 ])
180+ if len (c ) > 2 :
181+ d [- 1 ] += c [2 ]
182+
183+ if len (c ) > 3 :
184+ d .append (c [3 ])
185+ if len (c ) > 4 :
186+ d [- 1 ] += c [4 ]
187+
188+ if len (c ) > 5 :
189+ d .append (c [5 ])
190+ if len (c ) > 6 :
191+ d [- 1 ] += c [6 ]
192+
193+ if len (c ) > 7 :
194+ d .append (c [7 ])
195+ if len (c ) > 8 :
196+ d [- 1 ] += c [8 ]
197+
198+ if len (c ) > 9 :
199+ d .append (c [9 ])
200+
201+ new_chunks .append (d )
202+
203+ chunks = new_chunks
204+
205+ # flatten
206+ chunks = [item for sublist in chunks for item in sublist ]
207+
208+ # remove None (continuation blocks)
209+ chunks = [chunk for chunk in chunks if chunk is not None ]
210+
211+ # strip chunks
133212 chunks = [chunk .strip () for chunk in chunks ]
134213
135214 keyword = chunks [0 ]
136215
137216 # Points
138- if keyword == "GRID" :
217+ if keyword in [ "GRID" , "GRID*" ] :
139218 point_id = int (chunks [1 ])
140219 pref = chunks [2 ].strip ()
141220 if len (pref ) > 0 :
142221 point_refs .append (int (pref ))
143222 points_id .append (point_id )
144223 points .append ([_nastran_string_to_float (i ) for i in chunks [3 :6 ]])
145- elif keyword == "GRID*" : # large field format: 8 + 16*4 + 8
146- point_id = int (chunks [1 ] + chunks [2 ])
147- pref = (chunks [3 ] + chunks [4 ]).strip ()
148- if len (pref ) > 0 :
149- point_refs .append (int (pref ))
150- points_id .append (point_id )
151- chunks2 = _chunk_line (next_line )
152- next_line = f .readline ()
153- points .append (
154- [
155- _nastran_string_to_float (i + j )
156- for i , j in [chunks [5 :7 ], chunks [7 :9 ], chunks2 [1 :3 ]]
157- ]
158- )
159224
160225 # CellBlock
161226 elif keyword in nastran_to_meshio_type :
@@ -180,7 +245,7 @@ def add_cell(nastran_type, cell, cell_type, cell_ref):
180245 cell = [item for item in cell if item != "" ]
181246
182247 if cell is not None :
183- add_cell (keyword , cell , cell_type , cell_ref )
248+ add_cell (keyword , cell , cell_ref )
184249
185250 # Convert to numpy arrays
186251 points = np .array (points )
@@ -209,10 +274,20 @@ def add_cell(nastran_type, cell, cell_type, cell_ref):
209274
210275# There are two basic categories of input data formats in NX Nastran:
211276#
212- # "Free" format data, in which the data fields are simply separated by commas. This type of data is known as free field data.
213- # "Fixed" format data, in which your data must be aligned in columns of specific width. There are two subcategories of fixed format data that differ based on the size of the fixed column width:
214- # Small field format, in which a single line of data is divided into 10 fields that can contain eight characters each.
215- # Large field format, in which a single line of input is expanded into two lines The first and last fields on each line are eight columns wide, while the intermediate fields are sixteen columns wide. The large field format is useful when you need greater numerical accuracy.
277+ # - "Free" format data, in which the data fields are simply separated by
278+ # commas. This type of data is known as free field data.
279+ #
280+ # - "Fixed" format data, in which your data must be aligned in columns of
281+ # specific width. There are two subcategories of fixed format data that differ
282+ # based on the size of the fixed column width:
283+ #
284+ # - Small field format, in which a single line of data is divided into 10
285+ # fields that can contain eight characters each.
286+ #
287+ # - Large field format, in which a single line of input is expanded into
288+ # two lines The first and last fields on each line are eight columns wide,
289+ # while the intermediate fields are sixteen columns wide. The large field
290+ # format is useful when you need greater numerical accuracy.
216291#
217292# See: https://docs.plm.automation.siemens.com/data_services/resources/nxnastran/10/help/en_US/tdocExt/pdf/User.pdf
218293
@@ -262,7 +337,8 @@ def write(filename, mesh, point_format="fixed-large", cell_format="fixed-small")
262337 for point_id , x in enumerate (points ):
263338 fx = [float_fmt (k ) for k in x ]
264339 pref = str (point_refs [point_id ]) if point_refs is not None else ""
265- f .write (grid_fmt .format (point_id + 1 , pref , fx [0 ], fx [1 ], fx [2 ]))
340+ string = grid_fmt .format (point_id + 1 , pref , fx [0 ], fx [1 ], fx [2 ])
341+ f .write (string )
266342
267343 # CellBlock
268344 cell_id = 0
@@ -285,6 +361,7 @@ def write(filename, mesh, point_format="fixed-large", cell_format="fixed-small")
285361 cell1 = cell + 1
286362 cell1 = _convert_to_nastran_ordering (cell1 , nastran_type )
287363 conn = sjoin .join (int_fmt .format (nid ) for nid in cell1 [:nipl1 ])
364+
288365 if len (cell1 ) > nipl1 :
289366 if cell_format == "free" :
290367 cflag1 = cflag3 = ""
@@ -312,40 +389,62 @@ def _float_rstrip(x, n=8):
312389
313390def _float_to_nastran_string (value , length = 16 ):
314391 """
315- Return a value in NASTRAN scientific notation.
392+ From
393+ <https://docs.plm.automation.siemens.com/data_services/resources/nxnastran/10/help/en_US/tdocExt/pdf/User.pdf>:
394+
395+ Real numbers, including zero, must contain a decimal point. You can enter
396+ real numbers in a variety of formats. For example, the following are all
397+ acceptable versions of the real number, seven:
398+ ```
399+ 7.0 .7E1 0.7+1
400+ .70+1 7.E+0 70.-1
401+ ```
402+
403+ This methods converts a float value into the corresponding string. Choose
404+ the variant with `E` to make the file less ambigious when edited by a
405+ human. (`5.-1` looks like 4.0, not 5.0e-1 = 0.5.)
406+
316407 Examples:
317- 1234.56789 --> "1.23456789 +3"
318- -0.1234 --> "-1.234 -1"
319- 3.1415926535897932 --> "3.14159265359 +0"
408+ 1234.56789 --> "1.23456789E +3"
409+ -0.1234 --> "-1.234E -1"
410+ 3.1415926535897932 --> "3.14159265359E +0"
320411 """
321- aux = length - 2
322- # sfmt = "{" + f":{length}s" + "}"
323- sfmt = "{" + ":s" + "}"
324- pv_fmt = "{" + f":{ length } .{ aux } e" + "}"
412+ out = np .format_float_scientific (value , exp_digits = 1 , precision = 11 ).replace (
413+ "e" , "E"
414+ )
415+ assert len (out ) <= 16
416+ return out
417+ # The following is the manual float conversion. Keep it around for a while in case
418+ # we still need it.
419+
420+ # aux = length - 2
421+ # # sfmt = "{" + f":{length}s" + "}"
422+ # sfmt = "{" + ":s" + "}"
423+ # pv_fmt = "{" + f":{length}.{aux}e" + "}"
325424
326- if value == 0.0 :
327- return sfmt .format ("0." )
425+ # if value == 0.0:
426+ # return sfmt.format("0.")
328427
329- python_value = pv_fmt .format (value ) # -1.e-2
330- svalue , sexponent = python_value .strip ().split ("e" )
331- exponent = int (sexponent ) # removes 0s
428+ # python_value = pv_fmt.format(value) # -1.e-2
429+ # svalue, sexponent = python_value.strip().split("e")
430+ # exponent = int(sexponent) # removes 0s
332431
333- sign = "-" if abs (value ) < 1.0 else "+"
432+ # sign = "-" if abs(value) < 1.0 else "+"
334433
335- # the exponent will be added later...
336- sexp2 = str (exponent ).strip ("-+" )
337- value2 = float (svalue )
434+ # # the exponent will be added later...
435+ # sexp2 = str(exponent).strip("-+")
436+ # value2 = float(svalue)
338437
339- # the plus 1 is for the sign
340- len_sexp = len (sexp2 ) + 1
341- leftover = length - len_sexp
342- leftover = leftover - 3 if value < 0 else leftover - 2
343- fmt = "{" + f":1.{ leftover :d} f" + "}"
438+ # # the plus 1 is for the sign
439+ # len_sexp = len(sexp2) + 1
440+ # leftover = length - len_sexp
441+ # leftover = leftover - 3 if value < 0 else leftover - 2
442+ # fmt = "{" + f":1.{leftover:d}f" + "}"
344443
345- svalue3 = fmt .format (value2 )
346- svalue4 = svalue3 .strip ("0" )
347- field = sfmt .format (svalue4 + sign + sexp2 )
348- return field
444+ # svalue3 = fmt.format(value2)
445+ # svalue4 = svalue3.strip("0")
446+ # field = sfmt.format(svalue4 + sign + sexp2)
447+ # return field
349448
350449
351450def _nastran_string_to_float (string ):
@@ -356,14 +455,17 @@ def _nastran_string_to_float(string):
356455 return float (string [0 ] + string [1 :].replace ("+" , "e+" ).replace ("-" , "e-" ))
357456
358457
359- def _chunk_line (line ):
360- if "," in line : # free format
361- chunks = line .split ("," )
362- else : # fixed format
363- CHUNK_SIZE = 8
364- chunks = [line [i : CHUNK_SIZE + i ] for i in range (0 , 72 , CHUNK_SIZE )]
365- # everything after the 9th chunk is ignored
366- return chunks [:9 ]
458+ def _chunk_line (line : str ) -> tuple [list [str ], bool ]:
459+ # remove terminal newline
460+ assert line [- 1 ] == "\n "
461+ line = line [:- 1 ]
462+ if "," in line :
463+ # free format
464+ return line .split ("," ), True
465+ # fixed format
466+ CHUNK_SIZE = 8
467+ chunks = [line [i : CHUNK_SIZE + i ] for i in range (0 , len (line ), CHUNK_SIZE )]
468+ return chunks , False
367469
368470
369471def _convert_to_vtk_ordering (cell , nastran_type ):
0 commit comments