Coverage for fields_writer.py: 73%

95 statements  

« 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 -*- 

3 

4""" 

5Helpers to write NASTRAN cards 

6""" 

7from math import floor, log10 

8 

9 

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

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 

38 

39 

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. 

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 

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. 

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 

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

175class DefaultDict(dict): 

176 def __missing__(self, key): 

177 return "" 

178 

179 

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 

217 

218 

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 

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 

245if __name__ == "__main__": 

246 import doctest 

247 

248 doctest.testmod(optionflags=doctest.ELLIPSIS)