Coverage for writers/femap_neutral.py: 17%

214 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-03-20 20:51 +0100

1""" 

2FEMAP neutral writer 

3""" 

4 

5import logging 

6import os 

7 

8from jinja2 import Environment, FileSystemLoader 

9from numtools.intzip import zip_list 

10 

11TPLPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") 

12 

13 

14def _renumber(ints, offset): 

15 """renumber and return an offseted list of integers and the old->new mapping 

16 

17 >>> _renumber([1, 2, 3], offset=100) 

18 ([101, 102, 103], {1: 101, 2: 102, 3: 103}) 

19 >>> _renumber([1, 2, 101], offset=100) 

20 ([1, 2, 101], {1: 1, 2: 2, 101: 101}) 

21 >>> _renumber([], offset=100) 

22 ([], {}) 

23 """ 

24 if not ints: 

25 return [], {} 

26 _ints = ints.copy() 

27 if max(ints) < offset: 

28 ints = [i + offset for i in ints] 

29 renumbering = dict(zip(_ints, ints)) 

30 return ints, renumbering 

31 

32 

33_DEFAULT_CLIPPING = ( 

34 #'$COM --- default clipping ---\n' 

35 "0,0,0,0,0.,0.,\n" # 4:: coclip: CC_on, CC_dof, CC_meth, CC_csys, CC_min, CC_max 

36 "0,0,\n" # 5:: plcip_meth, plcip_in 

37 "0,0,\n" # 6:: + plclip_on, plclip_neg 

38 "0.,0.,0.,\n" # + plclip_base (X, Y, Z) 

39 "0.,0.,0.,\n" # + plclip_norm (X, Y, Z) 

40 "0,0,\n" # 6:: + plclip_on, plclip_neg 

41 "0.,0.,0.,\n" # + plclip_base (X, Y, Z) 

42 "0.,0.,0.,\n" # + plclip_norm (X, Y, Z) 

43 "0,0,\n" # 6:: + plclip_on, plclip_neg 

44 "0.,0.,0.,\n" # + plclip_base (X, Y, Z) 

45 "0.,0.,0.,\n" # + plclip_norm (X, Y, Z) 

46 "0,0,\n" # 6:: + plclip_on, plclip_neg 

47 "0.,0.,0.,\n" # + plclip_base (X, Y, Z) 

48 "0.,0.,0.,\n" # + plclip_norm (X, Y, Z) 

49 "0,0,\n" # 6:: + plclip_on, plclip_neg 

50 "0.,0.,0.,\n" # + plclip_base (X, Y, Z) 

51 "0.,0.,0.,\n" # + plclip_norm (X, Y, Z) 

52 "0,0,\n" # 6:: + plclip_on, plclip_neg 

53 "0.,0.,0.,\n" # + plclip_base (X, Y, Z) 

54 "0.,0.,0.,\n" # + plclip_norm (X, Y, Z) 

55 #'$COM --- /end of default clipping ---\n' 

56) 

57FEMAP_ENTITIES = { 

58 # entity: (group_rule_ID, group_entity_ID) 

59 "gids": (7, 17), 

60 "eids": (8, 21), 

61 "mids": (9, 26), 

62 "pids": (10, 30), 

63 "pnt_ids": (1, 3), 

64 "txt_ids": (5, 14), 

65} 

66 

67 

68def _dump_group(group, femap_grp_id, com): 

69 """ 

70 return a string containing the FEMAP neutral definition for the group. 

71 """ 

72 # ======================================================================== 

73 # group's header 

74 # ======================================================================== 

75 str_ = "%s,1,0,\n" % int(femap_grp_id) # ID, refresh?, no_renumbering? 

76 str_ += "%s\n" % group["title"] # group title 

77 str_ += "0,0,0,\n" # 3:: min layer, max layer, Type of layer usage 

78 str_ += _DEFAULT_CLIPPING # clipping stuff 

79 # ======================================================================== 

80 # rules 

81 # ======================================================================== 

82 if com: 

83 str_ += "$COM --- RULES ---\n" 

84 str_ += "133,\n" # MAX number of rules 

85 for entity, ids, femap_attr in zip( 

86 group["entities"].keys(), group["entities"].values(), FEMAP_ENTITIES.values() 

87 ): 

88 if len(ids) == 0: 

89 continue 

90 if com: 

91 str_ += "$COM --- /%d/ %s ---\n" % (femap_attr[1], entity) 

92 str_ += "%s,\n" % femap_attr[1] 

93 items_list = zip_list(ids, couple_alone=0) 

94 nb_lines = len(items_list) 

95 if nb_lines > 0: 

96 tpl = "\n".join(nb_lines * ["%d,%d,1,1,"]) 

97 var = tuple((id_ for e in items_list for id_ in e)) 

98 str_ += tpl % var 

99 str_ += "\n" 

100 str_ += "-1,-1,-1,-1,\n" 

101 str_ += "-1,\n" # End of rule: last record 

102 # ======================================================================== 

103 # entities 

104 # ======================================================================== 

105 if com: 

106 str_ += "$COM --- ENTITIES ---\n" 

107 str_ += "28,\n" # MAX number of entities sets 

108 # Actual group definition 

109 for entity, ids, femap_attr in zip( 

110 group["entities"].keys(), group["entities"].values(), FEMAP_ENTITIES.values() 

111 ): 

112 if len(ids) == 0: 

113 continue 

114 if com: 

115 str_ += "$COM --- /%d/ %s ---\n" % (femap_attr[0], entity) 

116 str_ += "%s,\n" % femap_attr[0] 

117 nb_lines = len(ids) 

118 tpl = "\n".join(nb_lines * ["%d,"]) 

119 var = tuple(ids) 

120 str_ += tpl % var 

121 str_ += "\n-1,\n" 

122 # ======================================================================== 

123 # that's all, Folks! 

124 # ======================================================================== 

125 str_ += "-1,\n" 

126 return str_ 

127 

128 

129class Neutral(object): 

130 """ 

131 Class handling basic export to FEMAP neutral file 

132 """ 

133 

134 default_points_data = { 

135 "type": 0, 

136 "engine": 0, 

137 "csys": 0, 

138 "layer": 1, 

139 "color": 22, 

140 "mesh_size": 0.0, 

141 "propertyID": 0, 

142 "compositeCurveID": 0, 

143 } 

144 

145 def __init__(self, femap_neutral_vers="11.0", database_title=None): 

146 self.femap_neu_vers = femap_neutral_vers 

147 self.database_title = database_title 

148 # self._create_group_block() 

149 self.layers = {} # { Layer ID: ??? } 

150 # { group ID: {'title': <str>, # MANDATORY 

151 # 'entities':{'eids': set(), 'gids': set()}, # MANDATORY 

152 # 'reference': [grpID1, grpID2, ...] # OPTIONAL 

153 # }} 

154 self.groups = {} 

155 self.texts = {} 

156 self.ids_offset = 1 

157 logging.info(f"loading template from {TPLPATH}") 

158 self.tpl_env = Environment( 

159 autoescape=False, 

160 loader=FileSystemLoader(TPLPATH), 

161 trim_blocks=True, 

162 lstrip_blocks=True, 

163 ) 

164 

165 def _next_id(self, attr): 

166 ids = set(getattr(self, attr).keys()) 

167 return max(ids, default=self.ids_offset - 1) + 1 

168 

169 # ======================================================================== 

170 # groups jobs 

171 # ======================================================================== 

172 def next_group_id(self): 

173 """return the next available goup id""" 

174 return self._next_id("groups") 

175 

176 def add_group(self, title, entities=None, reference=None): 

177 """ 

178 * 'title': a string defining how the group will be shown in FEMAP 

179 * 'entities': dictionnary of entities to group ({'eids': ..., 'gids': ...}) 

180 * 'reference': OPTIONAL. iterable of referenced groups 

181 """ 

182 _entities = dict(zip(FEMAP_ENTITIES.keys(), (set() for i in FEMAP_ENTITIES))) 

183 if not entities: 

184 entities = {} 

185 _entities.update(entities) 

186 if not reference: 

187 reference = set() 

188 nextid = self.next_group_id() 

189 self.groups[nextid] = { 

190 "title": title, 

191 "entities": _entities, 

192 "reference": reference, 

193 } 

194 return nextid 

195 

196 def refers_groups(self, grpid, grpids): 

197 """modify an existing group `grpid` to referes some other groups.""" 

198 self.groups[grpid]["reference"] = grpids 

199 

200 def dump_groups(self, com): 

201 """write all groups to the neutral file""" 

202 grpids = sorted(list(self.groups.keys())) 

203 string = "" 

204 for grpid in grpids: 

205 group = self.groups[grpid] 

206 try: 

207 string += _dump_group(group, grpid, com) 

208 except: 

209 logging.critical(f"cannot dump group ID# {grpid} {group}") 

210 return string.strip("\n") 

211 

212 def dump_referenced_groups(self, com): 

213 msg = "" 

214 if com: 

215 msg += "$COM ---[ Referenced Groups ] ---" 

216 for grpid, group in self.groups.items(): 

217 if "reference" not in group or len(group["reference"]) == 0: 

218 continue 

219 msg += "%d\n" % grpid 

220 msg += ",\n".join(["%d" % g for g in group["reference"]]) + ",\n" 

221 msg += "-1,\n" 

222 return msg.strip("\n") 

223 

224 # ======================================================================== 

225 # layers jobs 

226 # ======================================================================== 

227 def next_layer_id(self): 

228 """return the next available goup id""" 

229 return self._next_id("layers") 

230 

231 def add_layer(self, title, color=30): 

232 """add a layer. `layer` is a dictionnary with 2 keys: 

233 * 'title': a string defining how the group will be shown in FEMAP 

234 * 'color': OPTIONAL. Defaulted to XXX 

235 """ 

236 nextid = self.next_layer_id() 

237 self.layers[nextid] = {"title": title, "color": color} 

238 return nextid 

239 

240 def dump_layers(self, com): 

241 msg = "" 

242 layers = sorted(self.layers.keys()) 

243 if com: 

244 msg += "$COM ---[ Layers ] ---" 

245 for layerID in layers: 

246 msg += "%d, %d,\n" % (layerID, self.layers[layerID]["color"]) 

247 msg += "%s,\n" % self.layers[layerID]["title"] 

248 return msg.strip("\n") 

249 

250 # ======================================================================== 

251 # texts jobs 

252 # ======================================================================== 

253 def next_text_id(self): 

254 """return the next available goup id""" 

255 return self._next_id("texts") 

256 

257 def add_text( 

258 self, 

259 text, 

260 pointer_loc, 

261 text_offset=(-1, -1, -1), 

262 color=124, 

263 back_color=0, 

264 bord_color=14, 

265 font=1, 

266 layer=1, 

267 groupID=None, 

268 ): 

269 """add a text""" 

270 data = { 

271 "text": text, 

272 "color": color, 

273 "back_color": back_color, 

274 "bord_color": bord_color, 

275 "font": font, 

276 "layerID": layer, 

277 "model_positioning": 1, 

278 "horz_just": 1, # left 

279 "vert_just": 0, # center 

280 "visible": 1, 

281 "viewID": 1, 

282 "draw_pointer": 1, 

283 "draw_border": 1, 

284 "pointer_loc": pointer_loc, 

285 "text_position": [e1 + e2 for e1, e2 in zip(pointer_loc, text_offset)], 

286 "groupID": groupID, 

287 } 

288 nextid = self.next_text_id() 

289 self.texts[nextid] = data 

290 if groupID: 

291 self.groups[groupID]["entities"]["txt_ids"] |= set((nextid,)) 

292 return nextid 

293 

294 def dump_texts(self): 

295 """ """ 

296 msg = "" 

297 txts = self.texts # .df_text.T.to_dict() 

298 # prepare template 

299 tpl = "{txtid:.0f}, {color:.0f}, {back_color:.0f},\ 

300 {bord_color:.0f}, {font:.0f}, {layerID:.0f},\n" 

301 tpl += "{model_positioning:.0f}, {horz_just:.0f}, {vert_just:.0f},\ 

302 {visible:.0f}, {viewID:.0f}, {draw_pointer:.0f}, {draw_border:.0f},\n" 

303 tpl += "{tx}, {ty}, {tz},\n" 

304 tpl += "{px}, {py}, {pz},\n" 

305 tpl += "{text_lines:.0f},\n" 

306 tpl += "{text}\n" 

307 # prepare data 

308 for txtid, _data in txts.items(): 

309 _data = _data.copy() 

310 

311 _data["text_lines"] = len(_data["text"].split("\n")) 

312 _data["txtid"] = txtid 

313 _loc = _data.pop("text_position") 

314 _data["tx"] = _loc[0] 

315 _data["ty"] = _loc[1] 

316 _data["tz"] = _loc[2] 

317 _loc = _data.pop("pointer_loc") 

318 _data["px"] = _loc[0] 

319 _data["py"] = _loc[1] 

320 _data["pz"] = _loc[2] 

321 txt = tpl.format(**_data) 

322 msg += txt 

323 return msg.strip("\n") 

324 

325 # ======================================================================== 

326 # make 

327 # ======================================================================== 

328 def make(self, com): 

329 """prepare data for neutral file rendering""" 

330 data = { 

331 "com": com, 

332 # 'database_title': self.database_title, 

333 "neutral_version": self.femap_neu_vers, 

334 "layers": self.dump_layers(com=com), 

335 "groups": self.dump_groups(com=com), 

336 "referenced_groups": self.dump_referenced_groups(com=com), 

337 # 'points': self.dump_points(), 

338 "text": self.dump_texts(), 

339 } 

340 return data 

341 

342 def set_offset(self, offset): 

343 # apply offset to content 

344 self.ids_offset = offset 

345 # -------------------------------------------------------------------- 

346 # renumbering groups 

347 _, g_renumbering = _renumber(list(self.groups.keys()), offset) 

348 # self.groups = {g_renumbering[i]: v for i, v in self.groups.items()} 

349 # also renumber referenced groups 

350 for grpid, group in self.groups.copy().items(): 

351 newid = g_renumbering[grpid] 

352 self.groups[newid] = self.groups.pop(grpid) 

353 assert id(group) == id(self.groups[newid]) 

354 ref = group.get("reference", ()) 

355 if ref: 

356 logging.info("renumber references for group %d" % newid) 

357 group["reference"] = [g_renumbering[i] for i in group["reference"]] 

358 

359 # -------------------------------------------------------------------- 

360 # renumbering layers 

361 _, l_renumbering = _renumber(list(self.layers.keys()), offset) 

362 self.layers = {l_renumbering[i]: v for i, v in self.layers.items()} 

363 # -------------------------------------------------------------------- 

364 # renumbering texts 

365 _, t_renumbering = _renumber(list(self.texts.keys()), offset) 

366 for old_text_id, text in self.texts.copy().items(): 

367 new_text_id = t_renumbering[old_text_id] 

368 self.texts[new_text_id] = self.texts.pop(old_text_id) 

369 assert id(text) == id(self.texts[new_text_id]) 

370 text["layerID"] = l_renumbering[text["layerID"]] 

371 if text["groupID"] is not None: 

372 old_group_id = text["groupID"] 

373 new_group_id = g_renumbering[text["groupID"]] 

374 text["groupID"] = new_group_id 

375 self.groups[new_group_id]["entities"]["txt_ids"].remove(old_text_id) 

376 self.groups[new_group_id]["entities"]["txt_ids"].add(new_text_id) 

377 # self.texts = {t_renumbering[i]: v for i, v in self.texts.items()} 

378 

379 def dump_points(self): 

380 msg = "" 

381 pnts = self.model.df_points.T.to_dict() 

382 pnts = {k + self.ids_offset: v for k, v in pnts.items()} 

383 string = "{pntid}, {type}, {engine}, {csys}, {layer:.0f}, {color},\ 

384 {mesh_size}, {propertyID}, {compositeCurveID},\n" 

385 string += "{x}, {y}, {z},\n" 

386 for pntid, data in pnts.items(): 

387 if pntid == 0: 

388 continue 

389 _data = self.default_points_data.copy() 

390 _data.update(data) 

391 _data["pntid"] = pntid 

392 _data["color"] = int(_data["color"]) if _data["color"] else 22 

393 pnt = string.format(**_data) 

394 msg += pnt 

395 return msg.strip("\n") 

396 

397 # ========================================================================= 

398 # curves 

399 # ========================================================================= 

400 # 571 

401 # $com-------------------------- 

402 # 1,28686,0,10004,0,0.,0,0,0,0,0, 

403 # 0,0,0, 

404 # 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 

405 # 0.,0.,0., 

406 # 0.,0.,0.,0.,0.,0., 

407 # 479,480,0,0,0, 

408 # $com-------------------------- 

409 # -1 

410 

411 def get_category_by_grp(self, grp_obj): 

412 for category, groups in list(self._groups.items()): 

413 if grp_obj in groups: 

414 return category 

415 

416 def write(self, filepath=None, com=False): 

417 """write the file!""" 

418 data = self.make(com=com) 

419 neu = self.tpl_env.get_template("femap_neu.tpl").render(data) 

420 if not filepath: 

421 return neu 

422 with open(filepath, "w") as f: 

423 f.write(neu) 

424 return filepath 

425 

426 

427if __name__ == "__main__": 

428 import doctest 

429 

430 doctest.testmod(optionflags=doctest.ELLIPSIS) 

431 f = Femap_Neu() 

432 print(80 * "-") 

433 print(f.write()) 

434 print(80 * "-")