Coverage for mesh_api/mod.py: 11%

1"""Mesh Modification package"""

2

3 import logging

4 import numbers

5 from collections import defaultdict

6

7 import numpy as np

8 from numpy.lib import recfunctions as rfn

9

10 import nastranio.cards as cards

11 from nastranio.mesh_api.rtree_indexes import RTrees

12 from nastranio.utils import project_point, project_point_fast

13

14

15 class 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()

22

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)

58

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)

69

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

91

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 }

103

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 )

129

130 if clear_caches:

131 self.bulk["FORCE"].clear_caches()

132 self.bulk["MOMENT"].clear_caches()

133

134 def force_clear_caches(self):

135 self.bulk["FORCE"].clear_caches()

136 self.bulk["MOMENT"].clear_caches()

137

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

151

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

156

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

160

161

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

224

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