Coverage for fields_writer.py: 73%
95 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-20 20:51 +0100
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-20 20:51 +0100
1#!/usr/bin/env python2
2# -*- coding: utf-8 -*-
4"""
5Helpers to write NASTRAN cards
6"""
7from math import floor, log10
10def nbrows_by_fields(fields):
11 """return the number of rows that would be used to write provided
12 fields dictionnary
14 example: if two fields only are provided #17 and #23,
15 we need to write row 11->20 and 21->30
17 >>> nbrows_by_fields({17: 'robert', 23:'toto'})
18 2
19 """
20 keys = fields.keys()
21 kmax = max(keys)
22 kmin = min(keys)
23 # if kmin would be <=10, nb of rows would be
24 rowid_min = (kmin - 1) // 10
25 rowid_max = (kmax - 1) // 10
26 return (rowid_max - rowid_min) + 1
29def mantexp(f):
30 """return the mantissa and exponent of a number as a tuple
31 >>> mantexp(-2.3)
32 (-2.3, 0)
33 >>> mantexp(-1.123456789E-12)
34 (-1.123456789, -12)
35 """
36 exponent = int(floor(log10(abs(f)))) if f != 0 else 0
37 return f / 10**exponent, exponent
40def _trans_float(value, field_length=8):
41 """
42 return float's string representation in NASTRAN way. Try to reduce as much
43 as possible the string's length while keeping precision.
46 >>> _trans_float(15)
47 '15.'
48 >>> _trans_float(-15.999)
49 '-15.999'
50 >>> _trans_float(15.9999999999999999999)
51 '16.'
52 >>> _trans_float(12345678)
53 '1.2346+7'
54 >>> _trans_float(123456789)
55 '1.2346+8'
56 >>> _trans_float(-0.123456789)
57 '-.123457'
58 >>> _trans_float(-0.999999999997388)
59 '-1.'
60 >>> _trans_float(0.999999999997388)
61 '1.'
62 >>> _trans_float(270000.0)
63 '2.7000+5'
64 """
65 exponent = ""
66 available_chars = field_length
67 available_digits = field_length
68 # ========================================================================
69 # non-exponential numbers:
70 # * 0
71 # * range: ]-100000, -0.001]
72 # * range: [0.001, 100000[
73 # ========================================================================
74 # simple cases
75 if value == 0:
76 return "0."
77 elif -1 < value < -0.001:
78 # we loose two digits: "-."
79 tpl = "{{:.{}f}}".format(field_length - 2)
80 s = tpl.format(value)
81 # return *minus* sign (s[0], skip leading "0" (s[1]) and trailing 0)
82 if s[1] == "0":
83 return s[0] + s[2:].rstrip("0")
84 return s[0] + s[1:].rstrip("0")
85 elif 0.001 < value < 1:
86 # leading "0" can be omitted
87 tpl = "{{:.{}f}}".format(field_length - 1)
88 s = tpl.format(value)
89 if s[0] == "0":
90 return s[1:].rstrip("0")
91 return s[0:].rstrip("0")
93 # ------------------------------------------------------------------------
94 # more complex stuff
95 elif -100000 < value <= -1:
96 # negative number. We loose one digit for the '-' sign and one for the '.'
97 available_digits -= (
98 len(str(int(value))) + 1
99 ) # loose digits for integer part and dot
100 tpl = "{{0:.{}f}}".format(available_digits)
101 return tpl.format(value).rstrip("0")
102 elif 1 <= value < 100000:
103 available_digits -= (
104 len(str(int(value))) + 1
105 ) # loose digits for integer part and dot
106 tpl = "{{0:.{}f}}".format(available_digits)
107 return tpl.format(value).rstrip("0")
108 # ========================================================================
109 # exponential numbers:
110 # * range: -0.001, 0.001 # (excl. borns) small numbers
111 # * range: -inf, -100000 # (incl. borns) big negative numbers
112 # * range: 100000, +inf # (incl. borns) big negative numbers
113 # ========================================================================
114 else:
115 # use exponent notation
116 mantissa, exponent = mantexp(value)
117 available_chars = field_length - len(str(exponent)) - 2
118 if exponent > 0:
119 available_chars -= 1
120 if mantissa < 0:
121 available_chars -= 1
122 E_format = "{{mantissa:.{}f}}{{exponent:+d}}".format(available_chars)
123 return E_format.format(mantissa=mantissa, exponent=exponent)
126def _trans_int(val, field_length):
127 """
128 simply returns the integer as string (`str(val)`), except when resulting
129 length is more than allowed length. In this latter case, an exception is raised.
131 >>> _trans_int(1, 8)
132 '1'
133 """
134 s = str(val)
135 if len(s) <= field_length:
136 return s
137 # as per NASTRAN documentation, there shouldn't be any interger
138 # whose length is longer than field's length
139 raise ValueError(
140 "encountered integer (%d) wider than %d chars" % (val, field_length)
141 )
144def trans(val, field_length=8):
145 """translate field to NASTRAN compliant 8-characters fields
147 >>> checks = ((6250, '6250'),
148 ... (0.0, '0.'),
149 ... (6250.0, '6250.'),
150 ... (-0.123456789, "-.123457"),
151 ... (0.123456789, ".1234568"),
152 ... (0.0023148, '.0023148'),
153 ... (-1.987654e-12, '-1.99-12'),
154 ... (None, ""))
155 >>> err = []
156 >>> for val, exp in checks:
157 ... if trans(val) != exp:
158 ... err.append((val, exp, trans(val)))
159 >>> err
160 []
162 """
163 if val is None: # return blank field
164 res = ""
165 elif isinstance(val, float):
166 res = _trans_float(val, field_length=field_length)
167 elif isinstance(val, int):
168 res = _trans_int(val, field_length=field_length)
169 else:
170 res = val
171 # assert len(res) <= field_length
172 return res
175class DefaultDict(dict):
176 def __missing__(self, key):
177 return ""
180def fields_to_card(fields, leading="", sep=""):
181 """convert a single card dictionnary of fields to nastran card"""
182 # the `fields` dict should look like:
183 # {'fn1': str1, 'fn2': str2, ..., 'fn12': str3}
184 if len(fields["fn1"]) + len(leading) + len(sep) > 8:
185 raise ValueError('leading length is too long to fit with "%s"' % fields["fn1"])
186 tpl = (
187 "{leading}{{fn%d:{w1}}}{sep}{{fn%d:>{w}}}{sep}{{fn%d:>{w}}}{sep}{{fn%d:>{w}}}{sep}{{fn%d:>{w}}}{sep}"
188 "{{fn%d:>{w}}}{sep}{{fn%d:>{w}}}{sep}{{fn%d:>{w}}}{sep}{{fn%d:>{w}}}{sep}{{fn%d:{w}}}{sep}"
189 )
190 _d = {
191 "w1": 8 - len(leading) - len(sep),
192 "w": 8 - len(sep),
193 "leading": leading,
194 "sep": sep,
195 }
196 tpl = tpl.format(**_d)
197 # clean fields
198 for fieldcode, fieldvalue in fields.copy().items():
199 if not fieldvalue:
200 fields.pop(fieldcode)
201 # calculate the number of rows:
202 fieldmax = max([int(k[2:]) for k in fields.keys()])
203 nb_rows = fieldmax // 10 + 1
204 # populate continuation fields
205 for fnb in range(2, fieldmax):
206 if fnb % 10 == 0 or (fnb - 1) % 10 == 0:
207 fields["fn%d" % fnb] = "+"
208 # prepare the multiline template
209 tpls = []
210 for i in range(0, nb_rows):
211 ix = range(1 + i * 10, 11 + i * 10)
212 tpls.append(tpl % tuple(ix))
213 tpl = "\n".join(tpls)
214 lines = tpl.format_map(fields).split("\n")
215 lines = [l.strip() for l in lines]
216 return lines
219def get_field(field_nb):
220 """simply return `field_nb` when `field_nb` is not a continuation field nb (10, 11,
221 20, 21, etc.). Otherwise return next available field
223 >>> get_field(1)
224 1
225 >>> get_field(11)
226 12
227 >>> get_field(14)
228 14
229 >>> get_field(10)
230 12
231 >>> get_field(11)
232 12
233 >>> get_field(21)
234 22
235 """
236 if field_nb == 1:
237 return 1
238 if field_nb % 10 == 0:
239 return field_nb + 2
240 if (field_nb - 1) % 10 == 0:
241 return field_nb + 1
242 return field_nb
245if __name__ == "__main__":
246 import doctest
248 doctest.testmod(optionflags=doctest.ELLIPSIS)