Skip to content

Commit 0138cc8

Browse files
authored
Merge pull request #1303 from nschloe/nastran-fix
Nastran fix for small format
2 parents 6fbc398 + 9f32d06 commit 0138cc8

File tree

3 files changed

+170
-67
lines changed

3 files changed

+170
-67
lines changed

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = meshio
3-
version = 5.3.3
3+
version = 5.3.4
44
author = Nico Schlömer et al.
55
author_email = [email protected]
66
description = I/O for many mesh formats

src/meshio/_helpers.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ def write(filename, mesh: Mesh, file_format: str | None = None, **kwargs):
176176
if key in num_nodes_per_cell:
177177
if value.shape[1] != num_nodes_per_cell[key]:
178178
raise WriteError(
179-
f"Unexpected cells array shape {value.shape} for {key} cells."
179+
f"Unexpected cells array shape {value.shape} for {key} cells. "
180+
+ f"Expected shape [:, {num_nodes_per_cell[key]}]."
180181
)
181182
else:
182183
# we allow custom keys <https://github.com/nschloe/meshio/issues/501> and

src/meshio/nastran/_nastran.py

+167-65
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
I/O for Nastran bulk data.
33
"""
4+
from __future__ import annotations
5+
46
import numpy as np
57

68
from ..__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

313390
def _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

351450
def _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

369471
def _convert_to_vtk_ordering(cell, nastran_type):

0 commit comments

Comments
 (0)