midas-civil 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. midas_civil/_BoundaryChangeAssignment.py +278 -0
  2. midas_civil/__init__.py +51 -0
  3. midas_civil/_analysiscontrol.py +585 -0
  4. midas_civil/_boundary.py +888 -0
  5. midas_civil/_construction.py +1004 -0
  6. midas_civil/_element.py +1346 -0
  7. midas_civil/_group.py +337 -0
  8. midas_civil/_load.py +967 -0
  9. midas_civil/_loadcomb.py +159 -0
  10. midas_civil/_mapi.py +249 -0
  11. midas_civil/_material.py +1692 -0
  12. midas_civil/_model.py +522 -0
  13. midas_civil/_movingload.py +1479 -0
  14. midas_civil/_node.py +532 -0
  15. midas_civil/_result_table.py +929 -0
  16. midas_civil/_result_test.py +5455 -0
  17. midas_civil/_section/_TapdbSecSS.py +175 -0
  18. midas_civil/_section/__init__.py +413 -0
  19. midas_civil/_section/_compositeSS.py +283 -0
  20. midas_civil/_section/_dbSecSS.py +164 -0
  21. midas_civil/_section/_offsetSS.py +53 -0
  22. midas_civil/_section/_pscSS copy.py +455 -0
  23. midas_civil/_section/_pscSS.py +822 -0
  24. midas_civil/_section/_tapPSC12CellSS.py +565 -0
  25. midas_civil/_section/_unSupp.py +58 -0
  26. midas_civil/_settlement.py +161 -0
  27. midas_civil/_temperature.py +677 -0
  28. midas_civil/_tendon.py +1016 -0
  29. midas_civil/_thickness.py +147 -0
  30. midas_civil/_utils.py +529 -0
  31. midas_civil/_utilsFunc/__init__.py +0 -0
  32. midas_civil/_utilsFunc/_line2plate.py +636 -0
  33. midas_civil/_view.py +891 -0
  34. midas_civil/_view_trial.py +430 -0
  35. midas_civil/_visualise.py +347 -0
  36. midas_civil-1.4.1.dist-info/METADATA +74 -0
  37. midas_civil-1.4.1.dist-info/RECORD +40 -0
  38. midas_civil-1.4.1.dist-info/WHEEL +5 -0
  39. midas_civil-1.4.1.dist-info/licenses/LICENSE +21 -0
  40. midas_civil-1.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1346 @@
1
+ from ._mapi import MidasAPI,NX
2
+ from ._node import Node,nodeByID,nodesInGroup
3
+ from ._group import _add_node_2_stGroup,Group, _add_elem_2_stGroup
4
+ import numpy as np
5
+ # from scipy.interpolate import splev, splprep , interp1d , Akima1DInterpolator
6
+ from math import hypot
7
+ from ._utils import _convItem2List , _longestList,sFlatten
8
+ from colorama import Fore,Style
9
+ from typing import Literal
10
+
11
+ _meshType = Literal['Quad','Tri']
12
+
13
+ def _createSurface(points,mSize,tagID):
14
+ import gmsh
15
+ final_points, num_points = _dividePoints(points,mSize)
16
+
17
+ point_tags = []
18
+ for pt in final_points:
19
+ # print(pt)
20
+ point_tags.append(gmsh.model.occ.addPoint(pt[0],pt[1],pt[2],mSize))
21
+
22
+ line_tags = []
23
+ for i in range(num_points):
24
+ start = point_tags[i]
25
+ end = point_tags[(i+1) % num_points]
26
+ line_tags.append(gmsh.model.occ.addLine(start, end))
27
+ loop = gmsh.model.occ.addCurveLoop(line_tags)
28
+ surface = gmsh.model.occ.addPlaneSurface([loop],tag=tagID)
29
+ gmsh.model.occ.synchronize()
30
+ return surface
31
+
32
+ def _dividePoints(points,mSize):
33
+ num_points = len(points)
34
+ finer_points = [[points[0]]]
35
+
36
+ for q in range(num_points):
37
+ s_node = points[q]
38
+ e_node = points[(q+1)% num_points]
39
+
40
+ dist_node = hypot(e_node[0]-s_node[0],e_node[1]-s_node[1],e_node[2]-s_node[2])
41
+ n_div = max(int(dist_node//mSize),1)
42
+
43
+ int_nodes = np.linspace(s_node,e_node,n_div+1)
44
+ finer_points.append(int_nodes[1:])
45
+ # print(int_nodes)
46
+
47
+ final_points = sFlatten(finer_points)[:-1]
48
+ num_points = len(final_points)
49
+
50
+ return final_points,num_points
51
+
52
+ def _SInterp(angle,num_points):
53
+ ''' Angle -> Input list | Num Points -> Output length'''
54
+ from scipy.interpolate import interp1d , Akima1DInterpolator
55
+ angle = _convItem2List(angle)
56
+ if len(angle) == 1 :
57
+ angle.append(angle[0])
58
+ angle.append(angle[0])
59
+ if len(angle) == 2 :
60
+ angle.append(angle[-1])
61
+ angle[1] = (angle[0]+angle[2])*0.5
62
+
63
+ num_angle = len(angle)
64
+ angle_intrp_x = [0]
65
+ angle_intrp_y = [angle[0]]
66
+ for a in range(num_angle-1):
67
+ angle_intrp_x.append((a+1)*(num_points-1)/(num_angle-1))
68
+ angle_intrp_y.append(angle[a+1])
69
+
70
+ _alignment = Akima1DInterpolator(angle_intrp_x, angle_intrp_y,method='makima')
71
+ angle_intrp_func = interp1d(angle_intrp_x, angle_intrp_y)
72
+
73
+ angle_intrp_finalY = []
74
+ for i in range(num_points):
75
+ angle_intrp_finalY.append(_alignment(i))
76
+
77
+ return angle_intrp_finalY
78
+
79
+ def _interpolateAlignment(pointsArray,n_seg=10,deg=1,mSize=0,includePoint:bool=True,div_axis="L") -> list:
80
+ ''' Returns point list and beta angle list'''
81
+ from scipy.interpolate import splev, splprep
82
+ pointsArray = np.array(pointsArray)
83
+ x_p, y_p , z_p = pointsArray[:,0] , pointsArray[:,1] , pointsArray[:,2]
84
+
85
+ if deg < 1 :
86
+ deg = 1
87
+ if deg > len(pointsArray)-1:
88
+ deg = len(pointsArray)-1
89
+
90
+ #-- Actual length ----
91
+
92
+ tck, u = splprep([x_p, y_p, z_p], s=0, k=deg)
93
+
94
+ u_fine = np.linspace(0, 1, 500)
95
+ x_den, y_den, z_den = splev(u_fine, tck)
96
+
97
+ dx = np.diff(x_den)
98
+ dy = np.diff(y_den)
99
+ dz = np.diff(z_den)
100
+ dl=[]
101
+ for i in range(len(dx)):
102
+ dl.append(hypot(dx[i],dy[i],dz[i]))
103
+
104
+ cum_l = np.insert(np.cumsum(dl),0,0)
105
+ total_l = cum_l[-1]
106
+
107
+
108
+ if n_seg==0 or mSize!=0:
109
+ n_seg=int(total_l/mSize)
110
+
111
+
112
+ if div_axis == "X":
113
+ eq_x = np.linspace(x_p[0],x_p[-1],n_seg+1)
114
+ interp_u = np.interp(eq_x,x_den,u_fine)
115
+ elif div_axis == "Y":
116
+ eq_y = np.linspace(y_p[0],y_p[-1],n_seg+1)
117
+ interp_u = np.interp(eq_y,y_den,u_fine)
118
+ elif div_axis == "Z":
119
+ eq_z = np.linspace(z_p[0],z_p[-1],n_seg+1)
120
+ interp_u = np.interp(eq_z,z_den,u_fine)
121
+ else :
122
+ eq_len = np.linspace(0,total_l,n_seg+1)
123
+ interp_u = np.interp(eq_len,cum_l,u_fine)
124
+
125
+
126
+ if includePoint:
127
+ interp_u = np.sort(np.append(interp_u,u[1:-1])).tolist()
128
+
129
+ eq_u = 1/n_seg # for filtering close points
130
+
131
+ new_u = []
132
+ skip=0
133
+ for i in range(len(interp_u)-1):
134
+ if skip == 1:
135
+ skip = 0
136
+ continue
137
+ if interp_u[i+1]-interp_u[i] < 0.2*eq_u:
138
+ if interp_u[i] in u:
139
+ new_u.append(interp_u[i])
140
+ skip=1
141
+ else:
142
+ new_u.append(interp_u[i+1])
143
+ skip=1
144
+ else:
145
+ new_u.append(interp_u[i])
146
+ new_u.append(interp_u[-1])
147
+ else:
148
+ new_u = interp_u
149
+
150
+
151
+ interp_x, interp_y , interp_z = splev(new_u, tck)
152
+
153
+
154
+ align_fine_points = [ [round(x,6), round(y,6), round(z,6)] for x, y, z in zip(interp_x, interp_y , interp_z) ]
155
+
156
+ return align_fine_points
157
+
158
+
159
+
160
+ def _nodeDIST(a:Node,b:Node):
161
+ return round(hypot((a.X-b.X),(a.Y-b.Y),(a.Z-b.Z)),6)
162
+
163
+ def _nodeAngleVector(b:Node,a:Node):
164
+
165
+ Z_new = np.array([0.000001,0,1])
166
+ X_new = np.array([(a.X-b.X),(a.Y-b.Y),(a.Z-b.Z)])
167
+ Y_new = np.cross(Z_new, X_new)
168
+
169
+ Z_new = np.cross(X_new, Y_new) # Recomputing
170
+
171
+ X_new = X_new / (np.linalg.norm(X_new)+0.000001)
172
+ Y_new = Y_new / (np.linalg.norm(Y_new)+0.000001)
173
+ Z_new = Z_new / (np.linalg.norm(Z_new)+0.000001)
174
+
175
+
176
+ return [X_new,Y_new,Z_new]
177
+
178
+
179
+ def _triangleAREA(a:Node,b:Node,c:Node):
180
+ v1 = np.array([a.X-b.X,a.Y-b.Y,a.Z-b.Z])
181
+ v2 = np.array([b.X-c.X,b.Y-c.Y,b.Z-c.Z])
182
+ mag = np.linalg.norm(np.cross(v1, v2))
183
+ return float(0.5 * mag) , np.cross(v1, v2)/mag
184
+
185
+ def _calcVector(deltaLocation,angle=0): # Returns normalised local X,Y,Z for line
186
+ Z_new = np.array([0.000001,0,1])
187
+ X_new = np.array(deltaLocation)
188
+ Y_new = np.cross(Z_new, X_new)
189
+
190
+ Z_new = np.cross(X_new, Y_new) # Recomputing
191
+
192
+ X_new = X_new / (np.linalg.norm(X_new)+0.000001)
193
+ Y_new = Y_new / (np.linalg.norm(Y_new)+0.000001)
194
+ Z_new = Z_new / (np.linalg.norm(Z_new)+0.000001)
195
+
196
+ from scipy.spatial.transform import Rotation as R
197
+ p_y = np.array(Y_new)
198
+ p_z = np.array(Z_new)
199
+
200
+ axis = np.array(X_new)
201
+ theta = np.deg2rad(angle) # or radians directly
202
+ rot = R.from_rotvec(axis * theta) # axis-angle as rotation vector
203
+
204
+ rt_y = rot.apply(p_y) # rotated point around axis through origin
205
+ rt_z = rot.apply(p_z)
206
+
207
+ return [X_new,rt_y,rt_z]
208
+
209
+ def _rotatePT(pt,axis,deg):
210
+ from scipy.spatial.transform import Rotation as R
211
+ p = np.array(pt)
212
+ axis = np.array(axis)
213
+ theta = np.deg2rad(deg) # or radians directly
214
+ rot = R.from_rotvec(axis * theta) # axis-angle as rotation vector
215
+ return rot.apply(p) # rotated point around axis through origin
216
+
217
+ def _pointOffset(pts,yEcc=0,zEcc=0,angle=0):
218
+ from ._utils import _matchArray
219
+
220
+ angle2 = _matchArray(pts,angle)
221
+ yEcc2 = _matchArray(pts,yEcc)
222
+ zEcc2 = _matchArray(pts,zEcc)
223
+
224
+ norm = []
225
+ norm.append(_calcVector(np.subtract(pts[1],pts[0]),angle2[0])) # first X- along vector
226
+
227
+ for i in range(len(pts)-2): # Averaged X- along vector for middle points
228
+ X_new1 = np.array(np.subtract(pts[i+1],pts[i]))
229
+ X_new2 = np.array(np.subtract(pts[i+2],pts[i+1]))
230
+
231
+ X_new1 = X_new1 / (np.linalg.norm(X_new1)+0.000001)
232
+ X_new2 = X_new2 / (np.linalg.norm(X_new2)+0.000001)
233
+
234
+ norm.append(_calcVector(np.add(X_new1,X_new2),angle2[i+1]))
235
+
236
+ norm.append(_calcVector(np.subtract(pts[-1],pts[-2]),angle2[-1])) # last X- along vector
237
+
238
+ # print(norm)
239
+
240
+ pt_new = []
241
+ for i in range(len(pts)):
242
+ pt_new.append(pts[i]+yEcc2[i]*norm[i][1]+zEcc2[i]*norm[i][2])
243
+
244
+ return pt_new
245
+
246
+
247
+ def _ADD(self):
248
+ """
249
+ Adds an element to the main list. If the ID is 0, it auto-increments.
250
+ If the ID already exists, it replaces the existing element.
251
+ """
252
+
253
+ # ------------ ID assignment -----------------------
254
+ if NX.onlyNode == False :
255
+ id = int(self.ID)
256
+ # if not Element.ids:
257
+ # count = 1
258
+ # else:
259
+ # count = max(Element.ids) + 1
260
+
261
+ count = Element.maxID+1
262
+ if id == 0:
263
+ self.ID = count
264
+ Element.elements.append(self)
265
+ Element.ids.append(int(self.ID))
266
+ Element.maxID+= 1
267
+ elif id in Element.ids:
268
+ self.ID = int(id)
269
+ print(f'⚠️ Element with ID {id} already exists! It will be replaced.')
270
+ index = Element.ids.index(id)
271
+ Element.elements[index] = self
272
+ else:
273
+ self.ID = id
274
+ Element.elements.append(self)
275
+ Element.ids.append(int(self.ID))
276
+ if id > Element.maxID:
277
+ Element.maxID = id
278
+ Element.__elemDIC__[str(self.ID)] = self
279
+
280
+ # ------------ Group assignment -----------------------
281
+ if self._GROUP == "" :
282
+ pass
283
+ elif isinstance(self._GROUP, list):
284
+ for gpName in self._GROUP:
285
+ _add_elem_2_stGroup(self.ID,gpName)
286
+ for nd in self.NODE:
287
+ _add_node_2_stGroup(nd,gpName)
288
+ elif isinstance(self._GROUP, str):
289
+ _add_elem_2_stGroup(self.ID,self._GROUP)
290
+ # for nd in self.NODE:
291
+ _add_node_2_stGroup(self.NODE,self._GROUP)
292
+ else:
293
+ if self._GROUP == "" :
294
+ pass
295
+ elif isinstance(self._GROUP, list):
296
+ for gpName in self._GROUP:
297
+ for nd in self.NODE:
298
+ _add_node_2_stGroup(nd,gpName)
299
+ elif isinstance(self._GROUP, str):
300
+ for nd in self.NODE:
301
+ _add_node_2_stGroup(nd,self._GROUP)
302
+
303
+
304
+
305
+
306
+
307
+
308
+ def _updateElem(self):
309
+ """Sends a PUT request to update a single element in Midas."""
310
+ js2s = {'Assign': {self.ID: _Obj2JS(self)}}
311
+ MidasAPI('PUT', '/db/elem', js2s)
312
+ return js2s
313
+
314
+ def _Obj2JS(obj):
315
+ """Converts a Python element object to its JSON dictionary representation."""
316
+ # Base attributes common to many elements
317
+ js = {
318
+ "TYPE": obj.TYPE,
319
+ "MATL": obj.MATL,
320
+ "SECT": obj.SECT,
321
+ "NODE": obj.NODE,
322
+ }
323
+
324
+ # Add optional attributes if they exist on the object
325
+ if hasattr(obj, 'ANGLE'): js["ANGLE"] = obj.ANGLE
326
+ if hasattr(obj, 'STYPE'): js["STYPE"] = obj.STYPE
327
+
328
+ # Handle type-specific and subtype-specific attributes
329
+ if obj.TYPE == 'TENSTR': # Tension/Hook/Cable
330
+ # Tension-only (stype=1) - can have TENS parameter
331
+ if obj.STYPE == 1:
332
+ if hasattr(obj, 'TENS'): js["TENS"] = obj.TENS
333
+ if hasattr(obj, 'T_LIMIT'): js["T_LIMIT"] = obj.T_LIMIT
334
+ if hasattr(obj, 'T_bLMT'): js["T_bLMT"] = obj.T_bLMT
335
+
336
+ # Hook (stype=2) - has NON_LEN parameter
337
+ elif obj.STYPE == 2:
338
+ if hasattr(obj, 'NON_LEN'): js["NON_LEN"] = obj.NON_LEN
339
+
340
+ # Cable (stype=3) - has CABLE, NON_LEN, and TENS parameters
341
+ elif obj.STYPE == 3:
342
+ if hasattr(obj, 'CABLE'): js["CABLE"] = obj.CABLE
343
+ if hasattr(obj, 'NON_LEN'): js["NON_LEN"] = obj.NON_LEN
344
+ if hasattr(obj, 'TENS'): js["TENS"] = obj.TENS
345
+
346
+ elif obj.TYPE == 'COMPTR': # Compression/Gap
347
+ # Compression-only (stype=1) - can have TENS, T_LIMIT, T_bLMT
348
+ if obj.STYPE == 1:
349
+ if hasattr(obj, 'TENS'): js["TENS"] = obj.TENS
350
+ if hasattr(obj, 'T_LIMIT'): js["T_LIMIT"] = obj.T_LIMIT
351
+ if hasattr(obj, 'T_bLMT'): js["T_bLMT"] = obj.T_bLMT
352
+
353
+ # Gap (stype=2) - has NON_LEN parameter
354
+ elif obj.STYPE == 2:
355
+ if hasattr(obj, 'NON_LEN'): js["NON_LEN"] = obj.NON_LEN
356
+
357
+ return js
358
+
359
+ def _JS2Obj(id, js):
360
+ """Converts a JSON dictionary back into a Python element object during sync."""
361
+ elem_type = js.get('TYPE')
362
+
363
+ # Prepare arguments for constructors
364
+ args = {
365
+ 'id': int(id),
366
+ 'mat': js.get('MATL'),
367
+ 'sect': js.get('SECT'),
368
+ 'node': js.get('NODE'),
369
+ 'angle': js.get('ANGLE'),
370
+ 'stype': js.get('STYPE')
371
+ }
372
+
373
+ args['node'] = [x for x in args['node'] if x != 0]
374
+ nNodes = len(args['node'])
375
+ # Prepare individual parameters for optional/subtype-specific parameters
376
+ non_len = js.get('NON_LEN')
377
+ cable_type = js.get('CABLE')
378
+ tens = js.get('TENS')
379
+ t_limit = js.get('T_LIMIT')
380
+
381
+ if elem_type == 'BEAM':
382
+ Element.Beam(args['node'][0], args['node'][1], args['mat'], args['sect'], args['angle'], '', args['id'])
383
+ elif elem_type == 'TRUSS':
384
+ Element.Truss(args['node'][0], args['node'][1], args['mat'], args['sect'], args['angle'],'', args['id'])
385
+ elif elem_type == 'PLATE':
386
+ Element.Plate(args['node'][:nNodes], args['stype'], args['mat'], args['sect'], args['angle'], '', args['id'])
387
+ elif elem_type == 'TENSTR':
388
+ Element.Tension(args['node'][0], args['node'][1], args['stype'], args['mat'], args['sect'], args['angle'], '', args['id'], non_len, cable_type, tens, t_limit)
389
+ elif elem_type == 'COMPTR':
390
+ Element.Compression(args['node'][0], args['node'][1], args['stype'], args['mat'], args['sect'], args['angle'], '', args['id'], tens, t_limit, non_len)
391
+ elif elem_type == 'SOLID':
392
+ Element.Solid(nodes=args['node'][:nNodes], mat=args['mat'], sect=args['sect'],group='', id=args['id'])
393
+
394
+
395
+ class _helperELEM:
396
+ ID, TYPE, MATL,SECT,NODE,ANGLE,LENGTH,STYPE,AREA,NORMAL,CENTER,LOCALX = 0,0,0,0,0,0,0,0,0,0,0,0
397
+ class _common:
398
+ """Common base class for all element types."""
399
+ def __str__(self):
400
+ return str(f'ID = {self.ID} \nJSON : {_Obj2JS(self)}\n')
401
+
402
+ def update(self):
403
+ return _updateElem(self)
404
+
405
+ # --- Main Element Class ---
406
+ class Element():
407
+ """
408
+ Main class to create and manage structural elements like Beams, Trusses,
409
+ Plates, Tension/Compression-only elements, and Solids.
410
+ """
411
+ elements:list[_helperELEM] = []
412
+ ids:list[int] = []
413
+ maxID:int = 0
414
+ __elemDIC__ = {}
415
+
416
+
417
+ lastLoc = (0,0,0) #Last Location created using Beam element
418
+ '''Last Node Location created by Beam / Truss element - (x,y,z)'''
419
+
420
+ @classmethod
421
+ def json(cls):
422
+ json_data = {"Assign": {}}
423
+ for elem in cls.elements:
424
+ js = _Obj2JS(elem)
425
+ json_data["Assign"][elem.ID] = js
426
+ return json_data
427
+
428
+ @classmethod
429
+ def create(cls):
430
+ if cls.elements:
431
+ MidasAPI("PUT", "/db/ELEM", Element.json())
432
+
433
+ @staticmethod
434
+ def get():
435
+ return MidasAPI("GET", "/db/ELEM")
436
+
437
+ @staticmethod
438
+ def sync():
439
+ a = Element.get()
440
+ if a and 'ELEM' in a and a['ELEM']:
441
+ Element.elements = []
442
+ Element.ids = []
443
+ Element.__elemDIC__={}
444
+ for elem_id, data in a['ELEM'].items():
445
+ _JS2Obj(elem_id, data)
446
+
447
+ @staticmethod
448
+ def delete():
449
+ MidasAPI("DELETE", "/db/ELEM")
450
+ Element.clear()
451
+
452
+ @staticmethod
453
+ def clear():
454
+ Element.elements = []
455
+ Element.ids = []
456
+ Element.__elemDIC__={}
457
+
458
+ # --- Element Type Subclasses ---
459
+
460
+ class Beam(_common):
461
+
462
+ def __init__(self, i: int, j: int, mat: int = 1, sect: int = 1, angle: float = 0, group:str = "" , id: int = None,bLocalAxis:bool=False):
463
+ """
464
+ Creates a BEAM element for frame analysis.
465
+
466
+ Parameters:
467
+ i: Start node ID
468
+ j: End node ID
469
+ mat: Material property number (default 1)
470
+ sect: Section property number (default 1)
471
+ angle: Beta angle for section orientation in degrees (default 0.0)
472
+ group: Structure group of the element (str or list; 'SG1' or ['SG1','SG2'])
473
+ id: Element ID (default 0 for auto-increment)
474
+
475
+
476
+ Examples:
477
+ ```python
478
+ # Simple beam with default properties
479
+ Element.Beam(1, 2)
480
+
481
+ # Beam with specific material and section
482
+ Element.Beam(1, 2, mat=2, sect=3)
483
+
484
+ # Beam with 90° rotation (strong axis vertical)
485
+ Element.Beam(1, 2, mat=1, sect=1, angle=90.0)
486
+
487
+ # Beam with specific ID
488
+ Element.Beam(1, 2, mat=1, sect=1, angle=0.0, id=100)
489
+ ```
490
+ """
491
+ if id == None: id =0
492
+ self.ID = id
493
+ self.TYPE = 'BEAM'
494
+ self.MATL = mat
495
+ self.SECT = sect
496
+ self.NODE = [i, j]
497
+ self.ANGLE = angle
498
+ self._GROUP = group
499
+
500
+ _n1 = nodeByID(i)
501
+ _n2 = nodeByID(j)
502
+ self.LENGTH = _nodeDIST(_n1,_n2)
503
+ self.CENTER = np.average([_n1.LOC,_n2.LOC],0)
504
+ _dirVect = np.subtract(_n2.LOC,_n1.LOC)
505
+ self.LOCALX = _dirVect/(np.linalg.norm(_dirVect))
506
+
507
+ if bLocalAxis:
508
+ _tempAngle = _nodeAngleVector(_n1,_n2)
509
+ _n1.AXIS = np.add(_n1.AXIS,_tempAngle)
510
+ _n2.AXIS = np.add(_n2.AXIS,_tempAngle)
511
+
512
+ _norm1 = np.linalg.norm(_n1.AXIS ,axis=1,keepdims=True)
513
+ _n1.AXIS = _n1.AXIS /_norm1
514
+
515
+ _norm2 = np.linalg.norm(_n2.AXIS ,axis=1,keepdims=True)
516
+ _n2.AXIS = _n2.AXIS /_norm2
517
+
518
+ Element.lastLoc = (_n2.X,_n2.Y,_n2.Z)
519
+
520
+ _ADD(self)
521
+
522
+ @staticmethod
523
+ def SDL(s_loc:list,dir:list,l:float,n:int=1,mat:int=1,sect:int=1,angle:float=0, group:str = "" , id: int = None,bLocalAxis:bool=False): #CHANGE TO TUPLE
524
+ if id == None: id =0
525
+ if isinstance(s_loc,Node):
526
+ s_loc = (s_loc.X,s_loc.Y,s_loc.Z)
527
+
528
+ beam_nodes =[]
529
+ beam_obj = []
530
+ s_locc = np.array(s_loc)
531
+ unit_vec = np.array(dir)/np.linalg.norm(dir)
532
+
533
+ for i in range(n+1):
534
+ locc = s_locc+i*l*unit_vec/n
535
+ Enode=Node(locc[0].item(),locc[1].item(),locc[2].item())
536
+ beam_nodes.append(Enode.ID)
537
+ Element.lastLoc = (locc[0].item(),locc[1].item(),locc[2].item())
538
+ for i in range(n):
539
+ if id == 0 : id_new = 0
540
+ else: id_new = id+i
541
+ beam_obj.append(Element.Beam(beam_nodes[i],beam_nodes[i+1],mat,sect,angle,group,id_new,bLocalAxis))
542
+
543
+ return beam_obj
544
+
545
+
546
+ @staticmethod
547
+ def SE(s_loc:list,e_loc:list,n:int=1,mat:int=1,sect:int=1,angle:float=0, group:str = "" , id: int = None,bLocalAxis:bool=False):
548
+ if id == None: id =0
549
+ if isinstance(s_loc,Node):
550
+ s_loc = (s_loc.X,s_loc.Y,s_loc.Z)
551
+ if isinstance(e_loc,Node):
552
+ e_loc = (e_loc.X,e_loc.Y,e_loc.Z)
553
+
554
+ beam_nodes =[]
555
+ beam_obj = []
556
+ i_loc = np.linspace(s_loc,e_loc,n+1)
557
+ for i in range(n+1):
558
+ Enode=Node(i_loc[i][0].item(),i_loc[i][1].item(),i_loc[i][2].item())
559
+ beam_nodes.append(Enode.ID)
560
+ for i in range(n):
561
+ if id == 0 : id_new = 0
562
+ else: id_new = id+i
563
+ beam_obj.append(Element.Beam(beam_nodes[i],beam_nodes[i+1],mat,sect,angle,group,id_new,bLocalAxis))
564
+
565
+ return beam_obj
566
+
567
+ @staticmethod
568
+ def PLine(points_loc:list,n_div:int=0,deg:int=1,includePoint:bool=False,mat:int=1,sect:int=1,angle:float=0, group:str = "" , id: int = None,bLocalAxis:bool=False,div_axis:Literal['X','Y','Z','L']="L"):
569
+ '''
570
+ angle : float of list(float)
571
+ '''
572
+ if id == None: id =0
573
+ beam_nodes =[]
574
+ beam_obj = []
575
+ if n_div == 0 :
576
+ i_loc = points_loc
577
+ else:
578
+ i_loc = _interpolateAlignment(points_loc,n_div,deg,0,includePoint,div_axis)
579
+
580
+ num_points = len(i_loc)
581
+ angle_intrp_finalY = _SInterp(angle,num_points-1) #Beta Angle to be applied to Elements So, n-1
582
+
583
+ for i in i_loc:
584
+ Enode=Node(i[0],i[1],i[2])
585
+ beam_nodes.append(Enode.ID)
586
+ for i in range(len(i_loc)-1):
587
+ if id == 0 : id_new = 0
588
+ else: id_new = id+i
589
+ beam_obj.append(Element.Beam(beam_nodes[i],beam_nodes[i+1],mat,sect,angle_intrp_finalY[i].item(),group,id_new,bLocalAxis))
590
+
591
+ return beam_obj
592
+
593
+ @staticmethod
594
+ def PLine2(points_loc:list,n_div:int=0,deg:int=1,includePoint:bool=False,mat:int=1,sect:int=1,angle:list[float]=0, group:str = "" , id: int = None,bLocalAxis:bool=False,div_axis:Literal['X','Y','Z','L']="L",yEcc:list[float]=0,zEcc:list[float]=0,bAngleInEcc:bool=True):
595
+ '''
596
+ Creates a polyline with Eccentricity considering the beta angle provided
597
+ angle , yEcc , zEcc : float or list(float)
598
+ [0,10] -> Angle at start = 0 | Angle at end = 10
599
+ [0,10,0] -> Angle at start = 0 | Angle at mid = 10 | Angle at end = 0
600
+ Inbetween values are **MAKIMA 1D** interpolated. (not cubic)
601
+ '''
602
+ from ._utils import _matchArray
603
+ if id == None: id =0
604
+ beam_nodes =[]
605
+ beam_obj = []
606
+ if n_div == 0 :
607
+ i_loc = points_loc
608
+ else:
609
+ i_loc = _interpolateAlignment(points_loc,n_div,deg,0,includePoint,div_axis)
610
+
611
+
612
+ num_points = len(i_loc)
613
+ if bAngleInEcc:
614
+ angle_intrp_Ecc = _SInterp(angle,num_points)
615
+ else:
616
+ angle_intrp_Ecc = _matchArray(i_loc,[0])
617
+ angle_intrp_finalY = _SInterp(angle,num_points-1) #Beta Angle to be applied to Elements So, n-1
618
+
619
+ yEcc_intrp = _SInterp(yEcc,num_points)
620
+ zEcc_intrp = _SInterp(zEcc,num_points)
621
+
622
+ i_loc2 = _pointOffset(i_loc,yEcc_intrp,zEcc_intrp,angle_intrp_Ecc)
623
+ for i in i_loc2:
624
+ Enode=Node(i[0],i[1],i[2])
625
+ beam_nodes.append(Enode.ID)
626
+
627
+
628
+ for i in range(len(i_loc2)-1):
629
+ if id == 0 : id_new = 0
630
+ else: id_new = id+i
631
+ beam_obj.append(Element.Beam(beam_nodes[i],beam_nodes[i+1],mat,sect,angle_intrp_finalY[i].item(),group,id_new,bLocalAxis))
632
+
633
+ return beam_obj
634
+
635
+ class Truss(_common):
636
+ def __init__(self, i: int, j: int, mat: int = 1, sect: int = 1, angle: float = 0, group = "" , id: int = None):
637
+ """
638
+ Creates a TRUSS element
639
+
640
+ Parameters:
641
+ i: Start node ID
642
+ j: End node ID
643
+ mat: Material property number (default 1)
644
+ sect: Section property number (default 1)
645
+ angle: Beta angle for section orientation in degrees (default 0.0)
646
+ group: Structure group of the element (str or list; 'SG1' or ['SG1','SG2'])
647
+ id: Element ID (default 0 for auto-increment)
648
+
649
+ Examples:
650
+ ```python
651
+ # Simple truss member
652
+ Element.Truss(1, 2)
653
+
654
+ # Truss with specific material and section
655
+ Element.Truss(1, 2, mat=3, sect=2)
656
+
657
+ # Diagonal truss member
658
+ Element.Truss(3, 4, mat=1, sect=1, id=50)
659
+ ```
660
+ """
661
+ if id == None: id =0
662
+ self.ID = id
663
+ self.TYPE = 'TRUSS'
664
+ self.MATL = mat
665
+ self.SECT = sect
666
+ self.NODE = [i, j]
667
+ self.ANGLE = angle
668
+ self._GROUP = group
669
+ _n1 = nodeByID(i)
670
+ _n2 = nodeByID(j)
671
+ self.LENGTH = _nodeDIST(_n1,_n2)
672
+ self.CENTER = np.average([_n1.LOC,_n2.LOC],0)
673
+ _dirVect = np.subtract(_n2.LOC,_n1.LOC)
674
+ self.LOCALX = _dirVect/(np.linalg.norm(_dirVect))
675
+
676
+ Element.lastLoc = (_n2.X,_n2.Y,_n2.Z)
677
+ _ADD(self)
678
+
679
+ @staticmethod
680
+ def SDL(s_loc:list,dir:list,l:float,n:int=1,mat:int=1,sect:int=1,angle:float=0, group = "" , id: int = None):
681
+ if id == None: id =0
682
+ if isinstance(s_loc,Node):
683
+ s_loc = (s_loc.X,s_loc.Y,s_loc.Z)
684
+
685
+ beam_nodes =[]
686
+ beam_obj =[]
687
+ s_locc = np.array(s_loc)
688
+ unit_vec = np.array(dir)/np.linalg.norm(dir)
689
+
690
+ for i in range(n+1):
691
+ locc = s_locc+i*l*unit_vec/n
692
+ Enode=Node(locc[0].item(),locc[1].item(),locc[2].item())
693
+ beam_nodes.append(Enode.ID)
694
+
695
+ for i in range(n):
696
+ if id == 0 : id_new = 0
697
+ else: id_new = id+i
698
+ beam_obj.append(Element.Truss(beam_nodes[i],beam_nodes[i+1],mat,sect,angle,group,id_new))
699
+
700
+ return beam_obj
701
+
702
+
703
+ @staticmethod
704
+ def SE(s_loc:list,e_loc:list,n:int=1,mat:int=1,sect:int=1,angle:float=0, group = "" , id: int = None):
705
+ if id == None: id =0
706
+ if isinstance(s_loc,Node):
707
+ s_loc = (s_loc.X,s_loc.Y,s_loc.Z)
708
+ if isinstance(e_loc,Node):
709
+ s_loc = (e_loc.X,e_loc.Y,e_loc.Z)
710
+
711
+ beam_nodes =[]
712
+ beam_obj = []
713
+ i_loc = np.linspace(s_loc,e_loc,n+1)
714
+ for i in range(n+1):
715
+ Enode=Node(i_loc[i][0].item(),i_loc[i][1].item(),i_loc[i][2].item())
716
+ beam_nodes.append(Enode.ID)
717
+
718
+ for i in range(n):
719
+ if id == 0 : id_new = 0
720
+ else: id_new = id+i
721
+ beam_obj.append(Element.Truss(beam_nodes[i],beam_nodes[i+1],mat,sect,angle,group,id_new))
722
+
723
+ return beam_obj
724
+
725
+ class Plate(_common):
726
+ def __init__(self, nodes: list, stype: int = 1, mat: int = 1, sect: int = 1, angle: float = 0, group = "" , id: int = None):
727
+ """
728
+ Creates a PLATE element.
729
+
730
+ Parameters:
731
+ nodes: List of node IDs [n1, n2, n3] for triangular or [n1, n2, n3, n4] for quadrilateral
732
+ stype: Plate subtype (1=Thick plate, 2=Thin plate, 3=With drilling DOF) (default 1)
733
+ mat: Material property number (default 1)
734
+ sect: Section (thickness) property number (default 1)
735
+ angle: Material angle for orthotropic materials in degrees (default 0.0)
736
+ group: Structure group of the element (str or list; 'SG1' or ['SG1','SG2'])
737
+ id: Element ID (default 0 for auto-increment)
738
+
739
+ Examples:
740
+ ```python
741
+ # Triangular thick plate
742
+ Element.Plate([1, 2, 3], stype=1, mat=1, sect=1)
743
+
744
+ # Quadrilateral thin plate
745
+ Element.Plate([1, 2, 3, 4], stype=2, mat=2, sect=1)
746
+
747
+ # Plate with drilling DOF for shell analysis
748
+ Element.Plate([5, 6, 7, 8], stype=3, mat=1, sect=2, angle=45.0)
749
+ ```
750
+ """
751
+ if id == None: id =0
752
+ self.ID = id
753
+ self.TYPE = 'PLATE'
754
+ self.MATL = mat
755
+ self.SECT = sect
756
+
757
+ self.ANGLE = angle
758
+ self.STYPE = stype
759
+ self._GROUP = group
760
+
761
+ uniq_nodes = list(dict.fromkeys(nodes))
762
+ self._NPOINT=len(uniq_nodes)
763
+ if len(uniq_nodes)==3:
764
+ self.NODE = uniq_nodes
765
+ _n1 = nodeByID(uniq_nodes[0])
766
+ _n2 = nodeByID(uniq_nodes[1])
767
+ _n3 = nodeByID(uniq_nodes[2])
768
+ self.CENTER = np.average([_n1.LOC,_n2.LOC,_n3.LOC],0)
769
+ self.AREA,self.NORMAL = _triangleAREA(_n1,_n2,_n3)
770
+ elif len(uniq_nodes)==4:
771
+ self.NODE = nodes
772
+ _n1 = nodeByID(uniq_nodes[0])
773
+ _n2 = nodeByID(uniq_nodes[1])
774
+ _n3 = nodeByID(uniq_nodes[2])
775
+ _n4 = nodeByID(uniq_nodes[3])
776
+ a1 , n1 = _triangleAREA(_n1,_n2,_n3)
777
+ a2 , n2 = _triangleAREA(_n3,_n4,_n1)
778
+ self.AREA = a1+a2
779
+ self.NORMAL = (n1+n2)/np.linalg.norm((n1+n2+0.000001))
780
+ self.CENTER = np.average([_n1.LOC,_n2.LOC,_n3.LOC,_n4.LOC],0)
781
+
782
+
783
+
784
+ _ADD(self)
785
+
786
+ @staticmethod
787
+ def fromPoints(points: list, meshSize:float=1.0,meshType:_meshType='Tri', innerPoints:list=None,stype: int = 1, mat: int = 1, sect: int = 1, angle: float = 0, group = "" , id: int = None): #CHANGE TO TUPLE
788
+ # INPUTS POINTS and create a triangular/quad meshing with given mesh size | If meshSize = 0 , half of shortest length will be taken as mesh size
789
+
790
+ bHole = False
791
+ import gmsh
792
+ gmsh.initialize()
793
+ gmsh.option.setNumber("General.Terminal", 0)
794
+
795
+ surface_Main = _createSurface(points,meshSize,1)
796
+ if innerPoints:
797
+ surface_Hole = _createSurface(innerPoints,meshSize,2)
798
+ surface_Final = gmsh.model.occ.cut([(2,1)], [(2,2)], removeObject=True, removeTool=True)
799
+ bHole = True
800
+
801
+
802
+ gmsh.model.occ.synchronize()
803
+
804
+ if meshType == 'Quad':
805
+ if not bHole:
806
+ gmsh.option.setNumber("Mesh.Algorithm", 11) # WITHOUT HOLE
807
+ gmsh.option.setNumber("Mesh.MeshSizeMin", 2*meshSize)
808
+
809
+ else:
810
+ gmsh.option.setNumber("Mesh.Algorithm", 1) # WITH HOLE
811
+ gmsh.option.setNumber("Mesh.RecombinationAlgorithm", 2)
812
+ gmsh.option.setNumber("Mesh.RecombineAll", 1)
813
+ gmsh.option.setNumber("Mesh.MeshSizeMin", 2*meshSize)
814
+ else:
815
+ gmsh.option.setNumber("Mesh.Algorithm", 1)
816
+ gmsh.option.setNumber("Mesh.MeshSizeMin", 1.5*meshSize)
817
+
818
+
819
+ gmsh.option.setNumber("Mesh.Smoothing", 3)
820
+ gmsh.model.mesh.generate(2)
821
+
822
+ _, node_coords, _ = gmsh.model.mesh.getNodes()
823
+ nodes = np.array(node_coords).reshape(-1, 3) # (N, 3) array
824
+
825
+ _, _, elemNodeTags = gmsh.model.mesh.getElements(2)
826
+ if meshType == 'Quad':
827
+ elemNODE = np.array(elemNodeTags).reshape(-1, 4)
828
+ else:
829
+ elemNODE = np.array(elemNodeTags).reshape(-1, 3)
830
+
831
+ # gmsh.fltk.run()
832
+ gmsh.finalize()
833
+
834
+ nID_list = []
835
+ for nd in nodes:
836
+ nID_list.append(Node(nd[0],nd[1],nd[2]).ID)
837
+
838
+ plate_obj = []
839
+ for elmNd in elemNODE:
840
+ plate_obj.append(Element.Plate([nID_list[int(x)-1] for x in elmNd],stype,mat,sect,angle,group,id))
841
+
842
+ return plate_obj
843
+
844
+ @staticmethod
845
+ def loftGroups(strGroups: list, stype: int = 1, mat: int = 1, sect: int = 1, angle: float = 0, group = "" , id: int = None,nDiv:int=1,bClose:bool=False): #CHANGE TO TUPLE
846
+ # INPUTS 2 or more structure groups to create rectangular plates between the nodes | No. of nodes should be same in the Str Group
847
+ """
848
+ INPUTS 2 or more structure groups to create rectangular plates between the nodes
849
+ No. of nodes should be same in the Str Group
850
+ """
851
+ if id == None: id =0
852
+ n_groups = len(strGroups)
853
+ if n_groups < 2 :
854
+ print("⚠️ No. of structure groups in Plate.loftGroups in less than 2")
855
+ return False
856
+ plate_obj = []
857
+ for ng in range(n_groups-1):
858
+ nID_A = nodesInGroup(strGroups[ng])
859
+ nID_B = nodesInGroup(strGroups[ng+1])
860
+ if bClose:
861
+ nID_A.append(nID_A[0])
862
+ nID_B.append(nID_B[0])
863
+
864
+ max_len = max(len(nID_A),len(nID_B))
865
+ if max_len < 2 :
866
+ print("⚠️ No. of nodes in Plate.loftGroups in less than 2")
867
+ return False
868
+
869
+ nID_A , nID_B = _longestList(nID_A , nID_B)
870
+
871
+ if nDiv == 1 :
872
+ for i in range(max_len-1):
873
+ pt_array = [nID_A[i],nID_B[i],nID_B[i+1],nID_A[i+1]]
874
+ plate_obj.append(Element.Plate(pt_array,stype,mat,sect,angle,group,id))
875
+ if nDiv > 1 :
876
+ nID_dic = {}
877
+ for j in range(nDiv+1):
878
+ nID_dic[j] = []
879
+ nID_dic[0] = nID_A
880
+ nID_dic[nDiv] = nID_B
881
+ for i in range(max_len):
882
+ loc0= nodeByID(nID_A[i]).LOC
883
+ loc1 = nodeByID(nID_B[i]).LOC
884
+ int_points = np.linspace(loc0,loc1,nDiv+1)
885
+
886
+ for j in range(nDiv-1):
887
+ nID_dic[j+1].append(Node(int_points[j+1][0],int_points[j+1][1],int_points[j+1][2]).ID)
888
+
889
+ for q in range(nDiv):
890
+ for i in range(max_len-1):
891
+ pt_array = [nID_dic[q][i],nID_dic[q+1][i],nID_dic[q+1][i+1],nID_dic[q][i+1]]
892
+ plate_obj.append(Element.Plate(pt_array,stype,mat,sect,angle,group,id))
893
+
894
+ return plate_obj
895
+
896
+ @staticmethod
897
+ def extrude(points: list,dir:list,nDiv:int=1,bClose:bool=False,inpType='XYZ', stype: int = 1, mat: int = 1, sect: int = 1, angle: float = 0, group = "" , id: int = None): #CHANGE TO TUPLE
898
+ # INPUTS 2 or more structure groups to create rectangular plates between the nodes | No. of nodes should be same in the Str Group
899
+ """
900
+ INPUTS 2 or more structure groups to create rectangular plates between the nodes
901
+ No. of nodes should be same in the Str Group
902
+ """
903
+ if id == None: id =0
904
+ nID_A = []
905
+ nID_B = []
906
+
907
+ if inpType == 'XYZ':
908
+
909
+ f_pt = np.add(points,dir)
910
+
911
+ for i,pt in enumerate(points):
912
+ nID_A.append(Node(pt[0],pt[1],pt[2]).ID)
913
+ nID_B.append(Node(f_pt[i][0],f_pt[i][1],f_pt[i][2]).ID)
914
+ if inpType == 'ID':
915
+ nID_A = points
916
+ pts_loc = [nodeByID(pt).LOC for pt in points]
917
+
918
+ f_pt = np.add(pts_loc,dir)
919
+
920
+ for i in range(len(points)):
921
+ nID_B.append(Node(f_pt[i][0],f_pt[i][1],f_pt[i][2]).ID)
922
+
923
+ if inpType == 'NODE':
924
+ nID_A = [pt.ID for pt in points]
925
+ pts_loc = [pt.LOC for pt in points]
926
+
927
+ f_pt = np.add(pts_loc,dir)
928
+
929
+ for i in range(len(points)):
930
+ nID_B.append(Node(f_pt[i][0],f_pt[i][1],f_pt[i][2]).ID)
931
+
932
+
933
+ if bClose:
934
+ nID_A.append(nID_A[0])
935
+ nID_B.append(nID_B[0])
936
+
937
+ max_len = len(nID_B)
938
+
939
+ plate_obj = []
940
+ if nDiv == 1 :
941
+ for i in range(max_len-1):
942
+ pt_array = [nID_A[i],nID_B[i],nID_B[i+1],nID_A[i+1]]
943
+ plate_obj.append(Element.Plate(pt_array,stype,mat,sect,angle,group,id))
944
+ if nDiv > 1 :
945
+ nID_dic = {}
946
+ for j in range(nDiv+1):
947
+ nID_dic[j] = []
948
+ nID_dic[0] = nID_A
949
+ nID_dic[nDiv] = nID_B
950
+ for i in range(max_len):
951
+ loc0= nodeByID(nID_A[i]).LOC
952
+ loc1 = nodeByID(nID_B[i]).LOC
953
+ int_points = np.linspace(loc0,loc1,nDiv+1)
954
+
955
+ for j in range(nDiv-1):
956
+ nID_dic[j+1].append(Node(int_points[j+1][0],int_points[j+1][1],int_points[j+1][2]).ID)
957
+
958
+ for q in range(nDiv):
959
+ for i in range(max_len-1):
960
+ pt_array = [nID_dic[q][i],nID_dic[q+1][i],nID_dic[q+1][i+1],nID_dic[q][i+1]]
961
+ plate_obj.append(Element.Plate(pt_array,stype,mat,sect,angle,group,id))
962
+
963
+ return plate_obj
964
+
965
+ class Tension(_common):
966
+ def __init__(self, i: int, j: int, stype: int, mat: int = 1, sect: int = 1, angle: float = 0, group = "" , id: int = None, non_len: float = None, cable_type: int = None, tens: float = None, t_limit: float = None):
967
+ """
968
+ Creates a TENSTR (Tension-only) element.
969
+
970
+ Parameters:
971
+ i: Start node ID
972
+ j: End node ID
973
+ stype: Tension element subtype (1=Tension-only, 2=Hook, 3=Cable)
974
+ mat: Material property number (default 1)
975
+ sect: Section property number (default 1)
976
+ angle: Beta angle for section orientation in degrees (default 0.0)
977
+ group: Structure group of the element (str or list; 'SG1' or ['SG1','SG2'])
978
+ id: Element ID (default 0 for auto-increment)
979
+ non_len: Non-linear length parameter for Hook/Cable (default None)
980
+ cable_type: Cable type for stype=3 (1=Pretension, 2=Horizontal, 3=Lu) (default None)
981
+ tens: Initial tension force or allowable compression (default None)
982
+ t_limit: Tension limit value. If provided, the tension limit flag is set to True. (default None)
983
+
984
+ Examples:
985
+ ```python
986
+ # Simple tension-only member
987
+ Element.Tension(1, 2, stype=1)
988
+
989
+ # Tension-only with allowable compression and a tension limit
990
+ Element.Tension(1, 2, stype=1, tens=0.5, t_limit=-15)
991
+
992
+ # Hook element with slack length
993
+ Element.Tension(3, 4, stype=2, non_len=0.5)
994
+
995
+ # Cable with initial tension and catenary effects
996
+ Element.Tension(5, 6, stype=3, cable_type=3, tens=1000.0, non_len=0.1)
997
+ ```
998
+ """
999
+ if id == None: id =0
1000
+ self.ID = id
1001
+ self.TYPE = 'TENSTR'
1002
+ self.MATL = mat
1003
+ self.SECT = sect
1004
+ self.NODE = [i, j]
1005
+ self.ANGLE = angle
1006
+ self.STYPE = stype
1007
+ self._GROUP = group
1008
+ _n1 = nodeByID(i)
1009
+ _n2 = nodeByID(j)
1010
+ self.LENGTH = _nodeDIST(_n1,_n2)
1011
+ _dirVect = np.subtract(_n2.LOC,_n1.LOC)
1012
+ self.LOCALX = _dirVect/(np.linalg.norm(_dirVect))
1013
+ Element.lastLoc = (_n2.X,_n2.Y,_n2.Z)
1014
+
1015
+ # Handle subtype-specific parameters
1016
+ if stype == 1: # Tension-only specific
1017
+ if tens is not None:
1018
+ self.TENS = tens
1019
+ if t_limit is not None:
1020
+ self.T_LIMIT = t_limit
1021
+ self.T_bLMT = True
1022
+
1023
+ elif stype == 2: # Hook specific
1024
+ if non_len is not None:
1025
+ self.NON_LEN = non_len
1026
+
1027
+ elif stype == 3: # Cable specific
1028
+ if cable_type is not None:
1029
+ self.CABLE = cable_type
1030
+ if non_len is not None:
1031
+ self.NON_LEN = non_len
1032
+ if tens is not None:
1033
+ self.TENS = tens
1034
+ _ADD(self)
1035
+
1036
+ class Compression(_common):
1037
+ def __init__(self, i: int, j: int, stype: int, mat: int = 1, sect: int = 1, angle: float = 0, group = "" , id: int = None, tens: float = None, t_limit: float = None, non_len: float = None):
1038
+ """
1039
+ Creates a COMPTR (Compression-only) element.
1040
+
1041
+ Parameters:
1042
+ i: Start node ID
1043
+ j: End node ID
1044
+ stype: Compression element subtype (1=Compression-only, 2=Gap)
1045
+ mat: Material property number (default 1)
1046
+ sect: Section property number (default 1)
1047
+ angle: Beta angle for section orientation in degrees (default 0.0)
1048
+ group: Structure group of the element (str or list; 'SG1' or ['SG1','SG2'])
1049
+ id: Element ID (default 0 for auto-increment)
1050
+ tens: Allowable tension or initial compression force (default None)
1051
+ t_limit: Compression limit value. If provided, the compression limit flag is set to True. (default None)
1052
+ non_len: Non-linear length parameter for gap (default None)
1053
+
1054
+ Examples:
1055
+ ```python
1056
+ # Simple compression-only member
1057
+ Element.Compression(1, 2, stype=1)
1058
+
1059
+ # Compression-only with tension limit and buckling limit
1060
+ Element.Compression(1, 2, stype=1, tens=27, t_limit=-15)
1061
+
1062
+ # Gap element with initial gap
1063
+ Element.Compression(3, 4, stype=2, non_len=0.25)
1064
+ ```
1065
+ """
1066
+ if id == None: id =0
1067
+ self.ID = id
1068
+ self.TYPE = 'COMPTR'
1069
+ self.MATL = mat
1070
+ self.SECT = sect
1071
+ self.NODE = [i, j]
1072
+ self.ANGLE = angle
1073
+ self.STYPE = stype
1074
+ self._GROUP = group
1075
+ _n1 = nodeByID(i)
1076
+ _n2 = nodeByID(j)
1077
+ self.LENGTH = _nodeDIST(_n1,_n2)
1078
+ _dirVect = np.subtract(_n2.LOC,_n1.LOC)
1079
+ self.LOCALX = _dirVect/(np.linalg.norm(_dirVect))
1080
+ Element.lastLoc = (_n2.X,_n2.Y,_n2.Z)
1081
+
1082
+ # Handle subtype-specific parameters
1083
+ if stype == 1: # Compression-only specific
1084
+ if tens is not None:
1085
+ self.TENS = tens
1086
+ if t_limit is not None:
1087
+ self.T_LIMIT = t_limit
1088
+ self.T_bLMT = True
1089
+
1090
+ elif stype == 2: # Gap specific
1091
+ if non_len is not None:
1092
+ self.NON_LEN = non_len
1093
+ _ADD(self)
1094
+
1095
+ class Solid(_common):
1096
+ def __init__(self, nodes: list, mat: int = 1, sect: int = 0, group = "" , id: int = None):
1097
+ """
1098
+ Creates a SOLID element for 3D analysis.
1099
+
1100
+ Parameters:
1101
+ nodes: List of node IDs defining the solid element
1102
+ - 4 nodes: Tetrahedral element
1103
+ - 6 nodes: Pentahedral element
1104
+ - 8 nodes: Hexahedral element
1105
+ mat: Material property number (default 1)
1106
+ group: Structure group of the element (str or list; 'SG1' or ['SG1','SG2'])
1107
+ id: Element ID (default 0 for auto-increment)
1108
+
1109
+ Examples:
1110
+ ```python
1111
+ # Tetrahedral solid element
1112
+ Element.Solid([1, 2, 3, 4], mat=1)
1113
+
1114
+ # Wedge solid element
1115
+ Element.Solid([1, 2, 3, 4, 5, 6], mat=2)
1116
+
1117
+ # Hexahedral solid element
1118
+ Element.Solid([1, 2, 3, 4, 5, 6, 7, 8], mat=1, id=200)
1119
+ ```
1120
+ """
1121
+ if id == None: id =0
1122
+ if len(nodes) not in [4, 6, 8]:
1123
+ raise ValueError("Solid element must have 4, 6, or 8 nodes.")
1124
+ self.ID = id
1125
+ self.TYPE = 'SOLID'
1126
+ self.MATL = mat
1127
+ self.SECT = sect # Solid elements don't use section properties
1128
+ self.NODE = nodes
1129
+ self._GROUP = group
1130
+ _ADD(self)
1131
+
1132
+
1133
+ #-----------------------------------------------Stiffness Scale Factor------------------------------
1134
+
1135
+ class StiffnessScaleFactor:
1136
+
1137
+ data = []
1138
+
1139
+ def __init__(self,
1140
+ element_id,
1141
+ area_sf: float = 1.0,
1142
+ asy_sf: float = 1.0,
1143
+ asz_sf: float = 1.0,
1144
+ ixx_sf: float = 1.0,
1145
+ iyy_sf: float = 1.0,
1146
+ izz_sf: float = 1.0,
1147
+ wgt_sf: float = 1.0,
1148
+ group: str = "",
1149
+ id: int = None):
1150
+ """
1151
+ element_id: Element ID(s) where scale factor is applied (can be int or list)
1152
+ area_sf: Cross-sectional area scale factor
1153
+ asy_sf: Effective Shear Area scale factor (y-axis)
1154
+ asz_sf: Effective Shear Area scale factor (z-axis)
1155
+ ixx_sf: Torsional Resistance scale factor (x-axis)
1156
+ iyy_sf: Area Moment of Inertia scale factor (y-axis)
1157
+ izz_sf: Area Moment of Inertia scale factor (z-axis)
1158
+ wgt_sf: Weight scale factor
1159
+ group: Group name (default "")
1160
+ id: Scale factor ID (optional, auto-assigned if None)
1161
+
1162
+ Examples:
1163
+ StiffnessScaleFactor(908, area_sf=0.5, asy_sf=0.6, asz_sf=0.7,
1164
+ ixx_sf=0.8, iyy_sf=0.8, izz_sf=0.9, wgt_sf=0.95)
1165
+
1166
+ """
1167
+
1168
+ # Check if group exists, create if not
1169
+ if group != "":
1170
+ chk = 0
1171
+ a = [v['NAME'] for v in Group.Boundary.json()["Assign"].values()]
1172
+ if group in a:
1173
+ chk = 1
1174
+ if chk == 0:
1175
+ Group.Boundary(group)
1176
+
1177
+ # Handle element_id as single int or list
1178
+ if isinstance(element_id, (list, tuple)):
1179
+ self.ELEMENT_IDS = list(element_id)
1180
+ else:
1181
+ self.ELEMENT_IDS = [element_id]
1182
+
1183
+ self.AREA_SF = area_sf
1184
+ self.ASY_SF = asy_sf
1185
+ self.ASZ_SF = asz_sf
1186
+ self.IXX_SF = ixx_sf
1187
+ self.IYY_SF = iyy_sf
1188
+ self.IZZ_SF = izz_sf
1189
+ self.WGT_SF = wgt_sf
1190
+ self.GROUP_NAME = group
1191
+
1192
+ # Auto-assign ID if not provided
1193
+ if id is None:
1194
+ self.ID = len(Element.StiffnessScaleFactor.data) + 1
1195
+ else:
1196
+ self.ID = id
1197
+
1198
+ # Add to static list
1199
+ Element.StiffnessScaleFactor.data.append(self)
1200
+
1201
+ @classmethod
1202
+ def json(cls):
1203
+ """
1204
+ Converts StiffnessScaleFactor data to JSON format
1205
+ """
1206
+ json_data = {"Assign": {}}
1207
+
1208
+ for scale_factor in cls.data:
1209
+ # Create scale factor item
1210
+ scale_factor_item = {
1211
+ "ID": scale_factor.ID,
1212
+ "AREA_SF": scale_factor.AREA_SF,
1213
+ "ASY_SF": scale_factor.ASY_SF,
1214
+ "ASZ_SF": scale_factor.ASZ_SF,
1215
+ "IXX_SF": scale_factor.IXX_SF,
1216
+ "IYY_SF": scale_factor.IYY_SF,
1217
+ "IZZ_SF": scale_factor.IZZ_SF,
1218
+ "WGT_SF": scale_factor.WGT_SF,
1219
+ "GROUP_NAME": scale_factor.GROUP_NAME
1220
+ }
1221
+
1222
+ # Assign to each element ID
1223
+ for element_id in scale_factor.ELEMENT_IDS:
1224
+ if str(element_id) not in json_data["Assign"]:
1225
+ json_data["Assign"][str(element_id)] = {"ITEMS": []}
1226
+
1227
+ json_data["Assign"][str(element_id)]["ITEMS"].append(scale_factor_item)
1228
+
1229
+ return json_data
1230
+
1231
+ @classmethod
1232
+ def create(cls):
1233
+ """
1234
+ Sends all StiffnessScaleFactor data to the API
1235
+ """
1236
+ MidasAPI("PUT", "/db/essf", cls.json())
1237
+
1238
+ @classmethod
1239
+ def get(cls):
1240
+ """
1241
+ Retrieves StiffnessScaleFactor data from the API
1242
+ """
1243
+ return MidasAPI("GET", "/db/essf")
1244
+
1245
+ @classmethod
1246
+ def sync(cls):
1247
+ """
1248
+ Updates the StiffnessScaleFactor class with data from the API
1249
+ """
1250
+ cls.data = []
1251
+ response = cls.get()
1252
+
1253
+ if response != {'message': ''}:
1254
+ processed_ids = set() # To avoid duplicate processing
1255
+
1256
+ for element_data in response.get("ESSF", {}).items():
1257
+ for item in element_data.get("ITEMS", []):
1258
+ scale_factor_id = item.get("ID", 1)
1259
+
1260
+ # Skip if already processed (for multi-element scale factors)
1261
+ if scale_factor_id in processed_ids:
1262
+ continue
1263
+
1264
+ # Find all elements with the same scale factor ID
1265
+ element_ids = []
1266
+ for eid, edata in response.get("ESSF", {}).items():
1267
+ for eitem in edata.get("ITEMS", []):
1268
+ if eitem.get("ID") == scale_factor_id:
1269
+ element_ids.append(int(eid))
1270
+
1271
+ # Create StiffnessScaleFactor object
1272
+ Element.StiffnessScaleFactor(
1273
+ element_id=element_ids if len(element_ids) > 1 else element_ids[0],
1274
+ area_sf=item.get("AREA_SF", 1.0),
1275
+ asy_sf=item.get("ASY_SF", 1.0),
1276
+ asz_sf=item.get("ASZ_SF", 1.0),
1277
+ ixx_sf=item.get("IXX_SF", 1.0),
1278
+ iyy_sf=item.get("IYY_SF", 1.0),
1279
+ izz_sf=item.get("IZZ_SF", 1.0),
1280
+ wgt_sf=item.get("WGT_SF", 1.0),
1281
+ group=item.get("GROUP_NAME", ""),
1282
+ id=scale_factor_id
1283
+ )
1284
+
1285
+ processed_ids.add(scale_factor_id)
1286
+
1287
+ @classmethod
1288
+ def delete(cls):
1289
+ """
1290
+ Deletes all stiffness scale factors from the database and resets the class.
1291
+ """
1292
+ cls.data = []
1293
+ return MidasAPI("DELETE", "/db/essf")
1294
+
1295
+
1296
+
1297
+
1298
+ # ---- GET ELEMENT OBJECT FROM ID ----------
1299
+
1300
+ # def elemByID2(elemID:int) -> Element:
1301
+ # ''' Return Element object with the input ID '''
1302
+ # for elem in Element.elements:
1303
+ # if elem.ID == elemID:
1304
+ # return elem
1305
+
1306
+ # print(f'There is no element with ID {elemID}')
1307
+ # return None
1308
+
1309
+ def elemByID(elemID:int) -> _helperELEM:
1310
+ ''' Return Element object with the input ID '''
1311
+ try:
1312
+ return (Element.__elemDIC__[str(elemID)])
1313
+ except:
1314
+ print(Fore.RED +f'There is no element with ID {elemID}'+Style.RESET_ALL)
1315
+ return None
1316
+
1317
+ def elemsInGroup(groupName:str,unique:bool=True,reverse:bool=False,output:Literal['ID','ELEM']='ID') -> list[_helperELEM]:
1318
+ ''' Returns Element ID list or Element object list in a Structure Group '''
1319
+ groupNames = _convItem2List(groupName)
1320
+ elist = []
1321
+ for gName in groupNames:
1322
+ chk=1
1323
+ rev = reverse
1324
+ if gName[0] == '!':
1325
+ gName = gName[1:]
1326
+ rev = not rev
1327
+ for i in Group.Structure.Groups:
1328
+ if i.NAME == gName:
1329
+ chk=0
1330
+ eIDlist = i.ELIST
1331
+ if rev: eIDlist = list(reversed(eIDlist))
1332
+ elist.append(eIDlist)
1333
+ if chk:
1334
+ print(f'⚠️ "{gName}" - Structure group not found !')
1335
+ if unique:
1336
+ finalElist = list(dict.fromkeys(sFlatten(elist)))
1337
+ else:
1338
+ finalElist = sFlatten(elist)
1339
+
1340
+ if output == 'ELEM':
1341
+ finoutput = []
1342
+ for elm in finalElist:
1343
+ finoutput.append(elemByID(elm))
1344
+ finalElist:Element = finoutput
1345
+
1346
+ return finalElist