Coverage for mesh_api/mod.py: 11%
158 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"""Mesh Modification package"""
3import logging
4import numbers
5from collections import defaultdict
7import numpy as np
8from numpy.lib import recfunctions as rfn
10import nastranio.cards as cards
11from nastranio.mesh_api.rtree_indexes import RTrees
12from nastranio.utils import project_point, project_point_fast
15class Mod:
16 def set_registry(self, registry, index_elems=("0d", "1d", "2d", "3d")):
17 self.reg = registry
18 self.bulk = registry.container["bulk"]
19 self.mesh = registry.mesh
20 self.rtrees = RTrees(registry, index_elems=index_elems)
21 self.rtrees.build()
23 def node_move_to(self, gid, coords):
24 """move node `gid` to new coordinates"""
25 logging.info(f"move gid{gid} to {coords}")
26 griddata, rowno = self.bulk["GRID"].query_id(gid, with_loc=True)
27 rowno = rowno[0][0]
28 grid_coords = np.array(coords)
29 old_grid_coords = rfn.structured_to_unstructured(griddata[["X1", "X2", "X3"]])
30 old_grid_coords = old_grid_coords[0]
31 # collect current (pre-mod) impacted elements bboxes
32 impacted_eids = self.reg.mesh.gid2eids(gids=(gid,))[gid]
33 premod_elts_bboxes = {
34 eid: self.reg.mesh.get_eid_bbox(eid) for eid in impacted_eids
35 }
36 logging.debug(f"move gid#{gid} from {old_grid_coords}")
37 logging.debug(f" to {grid_coords=}")
38 logging.debug(f" {rowno=}")
39 logging.debug(f" impacted elts bboxes={premod_elts_bboxes.keys()}")
40 # =====================================================================
41 # update mesh
42 # =====================================================================
43 # change GRID carddata
44 self.bulk["GRID"].carddata["main"]["X1"][rowno] = grid_coords[0]
45 self.bulk["GRID"].carddata["main"]["X2"][rowno] = grid_coords[1]
46 self.bulk["GRID"].carddata["main"]["X3"][rowno] = grid_coords[2]
47 # ---------------------------------------------------------------------
48 # rebuild caches
49 self.bulk["GRID"].clear_caches()
50 # =====================================================================
51 # update rtrees
52 # =====================================================================
53 self.rtrees.update_grid(gid, old_coords=old_grid_coords, coords=grid_coords)
54 for eid, old_coords in premod_elts_bboxes.items():
55 coords = self.reg.mesh.get_eid_bbox(eid)
56 logging.info(f"moved {gid} from {old_coords} to {coords}")
57 self.rtrees.update_element(eid, old_coords=old_coords, coords=coords)
59 def node_move_by(self, gid, vector):
60 """move node `gid` by vector"""
61 logging.info("move gid{gid} by vector {coords}")
62 griddata, rowno = self.bulk["GRID"].query_id(gid, with_loc=True)
63 vector = np.array(vector)
64 old_coords = rfn.structured_to_unstructured(griddata[["X1", "X2", "X3"]])[0]
65 coords = old_coords + vector
66 logging.debug(f"move gid#{gid} by vector={vector}")
67 logging.debug(f" from {old_coords=} to {coords=}")
68 return self.node_move_to(gid, coords=coords)
70 def node_add(self, coords, gid=None, clear_caches=False):
71 """add grid <gid> at provided coords"""
72 if not gid:
73 gid = self.mesh.next_unused_gid()
74 else:
75 # check that node gid does not exist
76 if gid in self.mesh.gid2eids():
77 raise ValueError(f"node {gid} already exists")
78 # ---------------------------------------------------------------------
79 # update registry
80 self.reg.container["bulk"]["GRID"].append_sparams(
81 {"ID": gid, "X1": coords[0], "X2": coords[1], "X3": coords[2]}
82 )
83 # ---------------------------------------------------------------------
84 # update rtree
85 self.rtrees.add_grid(gid, coords=coords)
86 # ---------------------------------------------------------------------
87 # clear caches
88 if clear_caches:
89 self.bulk["GRID"].clear_caches()
90 return gid
92 def force_define_subcase(self, lcid, spcid=1, title=None):
93 if not title:
94 title = f"LCID#{lcid} SPC#{spcid}"
95 lcid_key = f"SUBCASE {lcid}"
96 cases = self.reg.container["cases"]
97 if lcid_key not in cases:
98 cases[lcid_key] = {
99 "SUBTITLE": title,
100 "SPC": spcid,
101 "LOAD": lcid,
102 }
104 def force_add(
105 self,
106 lcid,
107 gid,
108 scale=1.0,
109 fx=0.0,
110 fy=0.0,
111 fz=0.0,
112 mx=0.0,
113 my=0.0,
114 mz=0.0,
115 lcid_title=None,
116 clear_caches=False,
117 ):
118 for f in ("FORCE", "MOMENT"):
119 if f not in self.reg.container["bulk"]:
120 self.reg.container["bulk"][f] = getattr(cards, f)()
121 # ---------------------------------------------------------------------
122 # update registry
123 self.reg.container["bulk"]["FORCE"].append_sparams(
124 {"SID": lcid, "G": gid, "CID": 0, "F": scale, "N1": fx, "N2": fy, "N3": fz}
125 )
126 self.reg.container["bulk"]["MOMENT"].append_sparams(
127 {"SID": lcid, "G": gid, "CID": 0, "M": scale, "N1": mx, "N2": my, "N3": mz}
128 )
130 if clear_caches:
131 self.bulk["FORCE"].clear_caches()
132 self.bulk["MOMENT"].clear_caches()
134 def force_clear_caches(self):
135 self.bulk["FORCE"].clear_caches()
136 self.bulk["MOMENT"].clear_caches()
138 def elem1d_split(
139 self,
140 eid,
141 where,
142 rtree=None,
143 new_gid=None,
144 replicate_pins=False,
145 clear_caches=False,
146 ):
147 """
148 split 1d element:
149 * if where is a 0<where<1 number, use it as relative length
150 * if `where` is a list of numbers, assume it as new location
152 If `replicate_pins` is `False` (default`), then:
153 * existing element PB is set to None
154 * new element PA is set to None
155 * new element PB is set to initial element PB
157 If `replicate_pins` is `True` (default is `False`), then new
158 new element has the same PIN FLAGS `"PA"` and `"PB"` than
159 initial element
162 """
163 cardname = self.reg.mesh.eid2card(skipcache=True)[eid]
164 card = self.reg.container["bulk"][cardname]
165 if isinstance(where, numbers.Number):
166 if not 0 < where < 1:
167 raise ValueError(f"`where` ({where}) must be in ]0,1[ range.")
168 # ---------------------------------------------------------------------
169 # find location of split
170 _gid_a, _gid_b = self.reg.mesh.eid2gids(eids=(eid,), keep_order=True)[eid]
171 _length = self.reg.mesh.length(eids=(eid,))["data"][0]
172 _offset = self.reg.mesh.normals().loc[eid] * where * _length
173 _base_point = (
174 self.bulk["GRID"].coords(asdf=True)[["X", "Y", "Z"]].loc[_gid_a]
175 )
176 new_gid_coords = (_base_point + _offset).to_list()
177 else:
178 # assume a list/tuple of numbers is passed
179 new_gid_coords = where
180 initial_eid_bbox = self.reg.mesh.get_eid_bbox(eid)
181 # ---------------------------------------------------------------------
182 # create gid
183 new_gid = self.node_add(gid=new_gid, coords=new_gid_coords, clear_caches=False)
184 self.rtrees.add_grid(new_gid, coords=new_gid_coords)
185 # ---------------------------------------------------------------------
186 # change final node of existing element
187 # copy property from initial element
188 eid_ix, old_gid = card.update_gid(eid=eid, gidno=1, new_gid=new_gid)
189 old_properties = {k: v[eid_ix] for k, v in card.carddata["main"].items()}
190 new_properties = old_properties.copy()
191 # ---------------------------------------------------------------------
192 # create new element
193 new_eid = self.mesh.next_unused_eid()
194 gids_header = card.gids_header
195 new_properties[card.XID_FIELDNAME] = new_eid
196 new_properties[gids_header[0]] = new_gid
197 new_properties[gids_header[1]] = old_gid
198 # ---------------------------------------------------------------------
199 # if `not replicate_pins` only keep defined pins
200 if not replicate_pins:
201 pina, pinb = old_properties["PA"], old_properties["PB"]
202 if pina or pinb:
203 card.carddata["main"]["PB"][eid_ix] = None
204 new_properties["PA"] = None
205 new_properties["PB"] = pinb
206 self.reg.container["bulk"][card.name].append_sparams(new_properties)
207 # ---------------------------------------------------------------------
208 # update rtree
209 bbox = self.reg.mesh.get_eid_bbox(eid=new_eid)
210 self.rtrees.add_elem(cardname, new_eid, coords=bbox, rtree=rtree)
211 self.rtrees.update_element(
212 eid,
213 old_coords=initial_eid_bbox,
214 coords=self.reg.mesh.get_eid_bbox(eid=eid),
215 rtree=rtree,
216 )
217 # ---------------------------------------------------------------------
218 # clear caches
219 if clear_caches:
220 self.bulk["GRID"].clear_caches()
221 card.clear_caches()
222 self.reg.mesh.clear_caches()
223 return card, new_eid, new_gid
225 def multiple_elem1d_split(
226 self,
227 points,
228 cardname=None,
229 rtree=None,
230 eids=None,
231 margin=0.01,
232 round_t_digits=None,
233 ):
234 """split elements found close to each point"""
235 if eids:
236 rtree = self.reg.mesh.mod.rtrees.create_rtree(eids=eids)
237 if not round_t_digits:
238 # round `t` to number of digits of margin, therefore, defaulted to 2
239 round_t_digits = len(str(float(margin)).split(".")[-1])
240 _details = []
241 _historic_by_eid = defaultdict(list)
242 _prev_eids = defaultdict(set)
243 for i, point in enumerate(points):
244 prev_eids = self.rtrees.nearest_elements(
245 point, cardname=cardname, rtree=rtree, astype=list
246 )
247 prev_eid = next(iter(prev_eids))
248 if cardname:
249 cardnames = (cardname,)
250 geoms = self.mesh.geom_1d_elements(cardnames=cardnames)
251 elt = geoms.loc[prev_eid]
252 ga = elt[["XA", "YA", "ZA"]].values
253 gb = elt[["XB", "YB", "ZB"]].values
254 proj, t = project_point_fast(point, ga, gb)
255 # -----------------------------------------------------------------
256 # if t == 0 or == 1, len(prev_eids) should be >1
257 if margin < t < (1 - margin):
258 point = proj
259 card, new_eid, new_gid = self.elem1d_split(
260 eid=prev_eid, where=point, clear_caches=False, rtree=rtree
261 )
262 else:
263 if t < margin:
264 new_gid = int(elt["GA"])
265 point = elt[["XA", "YA", "ZA"]].tolist()
266 t = 0
267 else:
268 new_gid = int(elt["GB"])
269 point = elt[["XB", "YB", "ZB"]].tolist()
270 t = 1
271 new_eid = None
272 # -----------------------------------------------------------------
273 # keep track of initial element
274 if new_eid:
275 # if a new element has been created, append it to the list of
276 # old element children
277 for root_eid, _eids_ in _prev_eids.items():
278 if prev_eid in _eids_:
279 break
280 else:
281 # nothing found. It's an element that hasn't been already split
282 root_eid = prev_eid
283 _prev_eids[root_eid].add(new_eid)
284 else:
285 # no element split. An existing node is returned, so, let's return the
286 # existing element as mother and child element
287 root_eid = prev_eid
288 new_eid = prev_eid
289 # but maybe prev_eid comes from a previous plit...?
290 _historic_by_eid[new_eid].append(root_eid)
291 __details = {
292 "prev_eids": _historic_by_eid[new_eid], # ths whole historic
293 "prev_eid": root_eid, # really the last one
294 "eid": new_eid,
295 "gid": new_gid,
296 "XA": point[0],
297 "XB": point[1],
298 "XC": point[2],
299 "t": round(t, round_t_digits),
300 }
301 _details.append(__details)
302 # -----------------------------------------------------------------
303 # rebuild caches
304 self.bulk["GRID"].clear_caches()
305 card = self.reg.container["bulk"][cardname]
306 card.clear_caches()
307 self.reg.mesh.clear_caches()
308 return _details