Coverage for writers/femap_neutral.py: 17%

1"""

2 FEMAP neutral writer

3"""

4

5 import logging

6 import os

7

8 from jinja2 import Environment, FileSystemLoader

9 from numtools.intzip import zip_list

10

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

12

13

14 def _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)

57 FEMAP_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

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

129 class 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

427 if __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 * "-")