Coverage for fields_writer.py: 73%

1#!/usr/bin/env python2

2# -*- coding: utf-8 -*-

3

4"""

5 Helpers to write NASTRAN cards

6"""

7 from math import floor, log10

8

9

10 def nbrows_by_fields(fields):

11 """return the number of rows that would be used to write provided

12 fields dictionnary

13

14 example: if two fields only are provided #17 and #23,

15 we need to write row 11->20 and 21->30

16

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

27

28

29 def 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

38

39

40 def _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.

44

45

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")

92

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)

124

125

126 def _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.

130

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 )

142

143

144 def trans(val, field_length=8):

145 """translate field to NASTRAN compliant 8-characters fields

146

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 []

161

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

173

174

175 class DefaultDict(dict):

176 def __missing__(self, key):

177 return ""

178

179

180 def 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

217

218

219 def 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

222

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

243

244

245 if __name__ == "__main__":

246 import doctest

247

248 doctest.testmod(optionflags=doctest.ELLIPSIS)