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
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-20 20:51 +0100
1"""
2FEMAP neutral writer
3"""
5import logging
6import os
8from jinja2 import Environment, FileSystemLoader
9from numtools.intzip import zip_list
11TPLPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
14def _renumber(ints, offset):
15 """renumber and return an offseted list of integers and the old->new mapping
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
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}
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_
129class Neutral(object):
130 """
131 Class handling basic export to FEMAP neutral file
132 """
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 }
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 )
165 def _next_id(self, attr):
166 ids = set(getattr(self, attr).keys())
167 return max(ids, default=self.ids_offset - 1) + 1
169 # ========================================================================
170 # groups jobs
171 # ========================================================================
172 def next_group_id(self):
173 """return the next available goup id"""
174 return self._next_id("groups")
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
196 def refers_groups(self, grpid, grpids):
197 """modify an existing group `grpid` to referes some other groups."""
198 self.groups[grpid]["reference"] = grpids
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")
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")
224 # ========================================================================
225 # layers jobs
226 # ========================================================================
227 def next_layer_id(self):
228 """return the next available goup id"""
229 return self._next_id("layers")
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
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")
250 # ========================================================================
251 # texts jobs
252 # ========================================================================
253 def next_text_id(self):
254 """return the next available goup id"""
255 return self._next_id("texts")
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
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()
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")
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
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"]]
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()}
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")
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
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
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
427if __name__ == "__main__":
428 import doctest
430 doctest.testmod(optionflags=doctest.ELLIPSIS)
431 f = Femap_Neu()
432 print(80 * "-")
433 print(f.write())
434 print(80 * "-")