truss.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108
  1. """
  2. This module can be used to solve problems related
  3. to 2D Trusses.
  4. """
  5. from cmath import atan, inf
  6. from sympy.core.add import Add
  7. from sympy.core.evalf import INF
  8. from sympy.core.mul import Mul
  9. from sympy.core.symbol import Symbol
  10. from sympy.core.sympify import sympify
  11. from sympy import Matrix, pi
  12. from sympy.external.importtools import import_module
  13. from sympy.functions.elementary.miscellaneous import sqrt
  14. from sympy.matrices.dense import zeros
  15. import math
  16. from sympy.physics.units.quantities import Quantity
  17. from sympy.plotting import plot
  18. from sympy.utilities.decorator import doctest_depends_on
  19. from sympy import sin, cos
  20. __doctest_requires__ = {('Truss.draw'): ['matplotlib']}
  21. numpy = import_module('numpy', import_kwargs={'fromlist':['arange']})
  22. class Truss:
  23. """
  24. A Truss is an assembly of members such as beams,
  25. connected by nodes, that create a rigid structure.
  26. In engineering, a truss is a structure that
  27. consists of two-force members only.
  28. Trusses are extremely important in engineering applications
  29. and can be seen in numerous real-world applications like bridges.
  30. Examples
  31. ========
  32. There is a Truss consisting of four nodes and five
  33. members connecting the nodes. A force P acts
  34. downward on the node D and there also exist pinned
  35. and roller joints on the nodes A and B respectively.
  36. .. image:: truss_example.png
  37. >>> from sympy.physics.continuum_mechanics.truss import Truss
  38. >>> t = Truss()
  39. >>> t.add_node(("node_1", 0, 0), ("node_2", 6, 0), ("node_3", 2, 2), ("node_4", 2, 0))
  40. >>> t.add_member(("member_1", "node_1", "node_4"), ("member_2", "node_2", "node_4"), ("member_3", "node_1", "node_3"))
  41. >>> t.add_member(("member_4", "node_2", "node_3"), ("member_5", "node_3", "node_4"))
  42. >>> t.apply_load(("node_4", 10, 270))
  43. >>> t.apply_support(("node_1", "pinned"), ("node_2", "roller"))
  44. """
  45. def __init__(self):
  46. """
  47. Initializes the class
  48. """
  49. self._nodes = []
  50. self._members = {}
  51. self._loads = {}
  52. self._supports = {}
  53. self._node_labels = []
  54. self._node_positions = []
  55. self._node_position_x = []
  56. self._node_position_y = []
  57. self._nodes_occupied = {}
  58. self._member_lengths = {}
  59. self._reaction_loads = {}
  60. self._internal_forces = {}
  61. self._node_coordinates = {}
  62. @property
  63. def nodes(self):
  64. """
  65. Returns the nodes of the truss along with their positions.
  66. """
  67. return self._nodes
  68. @property
  69. def node_labels(self):
  70. """
  71. Returns the node labels of the truss.
  72. """
  73. return self._node_labels
  74. @property
  75. def node_positions(self):
  76. """
  77. Returns the positions of the nodes of the truss.
  78. """
  79. return self._node_positions
  80. @property
  81. def members(self):
  82. """
  83. Returns the members of the truss along with the start and end points.
  84. """
  85. return self._members
  86. @property
  87. def member_lengths(self):
  88. """
  89. Returns the length of each member of the truss.
  90. """
  91. return self._member_lengths
  92. @property
  93. def supports(self):
  94. """
  95. Returns the nodes with provided supports along with the kind of support provided i.e.
  96. pinned or roller.
  97. """
  98. return self._supports
  99. @property
  100. def loads(self):
  101. """
  102. Returns the loads acting on the truss.
  103. """
  104. return self._loads
  105. @property
  106. def reaction_loads(self):
  107. """
  108. Returns the reaction forces for all supports which are all initialized to 0.
  109. """
  110. return self._reaction_loads
  111. @property
  112. def internal_forces(self):
  113. """
  114. Returns the internal forces for all members which are all initialized to 0.
  115. """
  116. return self._internal_forces
  117. def add_node(self, *args):
  118. """
  119. This method adds a node to the truss along with its name/label and its location.
  120. Multiple nodes can be added at the same time.
  121. Parameters
  122. ==========
  123. The input(s) for this method are tuples of the form (label, x, y).
  124. label: String or a Symbol
  125. The label for a node. It is the only way to identify a particular node.
  126. x: Sympifyable
  127. The x-coordinate of the position of the node.
  128. y: Sympifyable
  129. The y-coordinate of the position of the node.
  130. Examples
  131. ========
  132. >>> from sympy.physics.continuum_mechanics.truss import Truss
  133. >>> t = Truss()
  134. >>> t.add_node(('A', 0, 0))
  135. >>> t.nodes
  136. [('A', 0, 0)]
  137. >>> t.add_node(('B', 3, 0), ('C', 4, 1))
  138. >>> t.nodes
  139. [('A', 0, 0), ('B', 3, 0), ('C', 4, 1)]
  140. """
  141. for i in args:
  142. label = i[0]
  143. x = i[1]
  144. x = sympify(x)
  145. y=i[2]
  146. y = sympify(y)
  147. if label in self._node_coordinates:
  148. raise ValueError("Node needs to have a unique label")
  149. elif [x, y] in self._node_coordinates.values():
  150. raise ValueError("A node already exists at the given position")
  151. else :
  152. self._nodes.append((label, x, y))
  153. self._node_labels.append(label)
  154. self._node_positions.append((x, y))
  155. self._node_position_x.append(x)
  156. self._node_position_y.append(y)
  157. self._node_coordinates[label] = [x, y]
  158. def remove_node(self, *args):
  159. """
  160. This method removes a node from the truss.
  161. Multiple nodes can be removed at the same time.
  162. Parameters
  163. ==========
  164. The input(s) for this method are the labels of the nodes to be removed.
  165. label: String or Symbol
  166. The label of the node to be removed.
  167. Examples
  168. ========
  169. >>> from sympy.physics.continuum_mechanics.truss import Truss
  170. >>> t = Truss()
  171. >>> t.add_node(('A', 0, 0), ('B', 3, 0), ('C', 5, 0))
  172. >>> t.nodes
  173. [('A', 0, 0), ('B', 3, 0), ('C', 5, 0)]
  174. >>> t.remove_node('A', 'C')
  175. >>> t.nodes
  176. [('B', 3, 0)]
  177. """
  178. for label in args:
  179. for i in range(len(self.nodes)):
  180. if self._node_labels[i] == label:
  181. x = self._node_position_x[i]
  182. y = self._node_position_y[i]
  183. if label not in self._node_coordinates:
  184. raise ValueError("No such node exists in the truss")
  185. else:
  186. members_duplicate = self._members.copy()
  187. for member in members_duplicate:
  188. if label == self._members[member][0] or label == self._members[member][1]:
  189. raise ValueError("The given node already has member attached to it")
  190. self._nodes.remove((label, x, y))
  191. self._node_labels.remove(label)
  192. self._node_positions.remove((x, y))
  193. self._node_position_x.remove(x)
  194. self._node_position_y.remove(y)
  195. if label in self._loads:
  196. self._loads.pop(label)
  197. if label in self._supports:
  198. self._supports.pop(label)
  199. self._node_coordinates.pop(label)
  200. def add_member(self, *args):
  201. """
  202. This method adds a member between any two nodes in the given truss.
  203. Parameters
  204. ==========
  205. The input(s) of the method are tuple(s) of the form (label, start, end).
  206. label: String or Symbol
  207. The label for a member. It is the only way to identify a particular member.
  208. start: String or Symbol
  209. The label of the starting point/node of the member.
  210. end: String or Symbol
  211. The label of the ending point/node of the member.
  212. Examples
  213. ========
  214. >>> from sympy.physics.continuum_mechanics.truss import Truss
  215. >>> t = Truss()
  216. >>> t.add_node(('A', 0, 0), ('B', 3, 0), ('C', 2, 2))
  217. >>> t.add_member(('AB', 'A', 'B'), ('BC', 'B', 'C'))
  218. >>> t.members
  219. {'AB': ['A', 'B'], 'BC': ['B', 'C']}
  220. """
  221. for i in args:
  222. label = i[0]
  223. start = i[1]
  224. end = i[2]
  225. if start not in self._node_coordinates or end not in self._node_coordinates or start==end:
  226. raise ValueError("The start and end points of the member must be unique nodes")
  227. elif label in self._members:
  228. raise ValueError("A member with the same label already exists for the truss")
  229. elif self._nodes_occupied.get((start, end)):
  230. raise ValueError("A member already exists between the two nodes")
  231. else:
  232. self._members[label] = [start, end]
  233. self._member_lengths[label] = sqrt((self._node_coordinates[end][0]-self._node_coordinates[start][0])**2 + (self._node_coordinates[end][1]-self._node_coordinates[start][1])**2)
  234. self._nodes_occupied[start, end] = True
  235. self._nodes_occupied[end, start] = True
  236. self._internal_forces[label] = 0
  237. def remove_member(self, *args):
  238. """
  239. This method removes members from the given truss.
  240. Parameters
  241. ==========
  242. labels: String or Symbol
  243. The label for the member to be removed.
  244. Examples
  245. ========
  246. >>> from sympy.physics.continuum_mechanics.truss import Truss
  247. >>> t = Truss()
  248. >>> t.add_node(('A', 0, 0), ('B', 3, 0), ('C', 2, 2))
  249. >>> t.add_member(('AB', 'A', 'B'), ('AC', 'A', 'C'), ('BC', 'B', 'C'))
  250. >>> t.members
  251. {'AB': ['A', 'B'], 'AC': ['A', 'C'], 'BC': ['B', 'C']}
  252. >>> t.remove_member('AC', 'BC')
  253. >>> t.members
  254. {'AB': ['A', 'B']}
  255. """
  256. for label in args:
  257. if label not in self._members:
  258. raise ValueError("No such member exists in the Truss")
  259. else:
  260. self._nodes_occupied.pop((self._members[label][0], self._members[label][1]))
  261. self._nodes_occupied.pop((self._members[label][1], self._members[label][0]))
  262. self._members.pop(label)
  263. self._member_lengths.pop(label)
  264. self._internal_forces.pop(label)
  265. def change_node_label(self, *args):
  266. """
  267. This method changes the label(s) of the specified node(s).
  268. Parameters
  269. ==========
  270. The input(s) of this method are tuple(s) of the form (label, new_label).
  271. label: String or Symbol
  272. The label of the node for which the label has
  273. to be changed.
  274. new_label: String or Symbol
  275. The new label of the node.
  276. Examples
  277. ========
  278. >>> from sympy.physics.continuum_mechanics.truss import Truss
  279. >>> t = Truss()
  280. >>> t.add_node(('A', 0, 0), ('B', 3, 0))
  281. >>> t.nodes
  282. [('A', 0, 0), ('B', 3, 0)]
  283. >>> t.change_node_label(('A', 'C'), ('B', 'D'))
  284. >>> t.nodes
  285. [('C', 0, 0), ('D', 3, 0)]
  286. """
  287. for i in args:
  288. label = i[0]
  289. new_label = i[1]
  290. if label not in self._node_coordinates:
  291. raise ValueError("No such node exists for the Truss")
  292. elif new_label in self._node_coordinates:
  293. raise ValueError("A node with the given label already exists")
  294. else:
  295. for node in self._nodes:
  296. if node[0] == label:
  297. self._nodes[self._nodes.index((label, node[1], node[2]))] = (new_label, node[1], node[2])
  298. self._node_labels[self._node_labels.index(node[0])] = new_label
  299. self._node_coordinates[new_label] = self._node_coordinates[label]
  300. self._node_coordinates.pop(label)
  301. if node[0] in self._supports:
  302. self._supports[new_label] = self._supports[node[0]]
  303. self._supports.pop(node[0])
  304. if new_label in self._supports:
  305. if self._supports[new_label] == 'pinned':
  306. if 'R_'+str(label)+'_x' in self._reaction_loads and 'R_'+str(label)+'_y' in self._reaction_loads:
  307. self._reaction_loads['R_'+str(new_label)+'_x'] = self._reaction_loads['R_'+str(label)+'_x']
  308. self._reaction_loads['R_'+str(new_label)+'_y'] = self._reaction_loads['R_'+str(label)+'_y']
  309. self._reaction_loads.pop('R_'+str(label)+'_x')
  310. self._reaction_loads.pop('R_'+str(label)+'_y')
  311. self._loads[new_label] = self._loads[label]
  312. for load in self._loads[new_label]:
  313. if load[1] == 90:
  314. load[0] -= Symbol('R_'+str(label)+'_y')
  315. if load[0] == 0:
  316. self._loads[label].remove(load)
  317. break
  318. for load in self._loads[new_label]:
  319. if load[1] == 0:
  320. load[0] -= Symbol('R_'+str(label)+'_x')
  321. if load[0] == 0:
  322. self._loads[label].remove(load)
  323. break
  324. self.apply_load(new_label, Symbol('R_'+str(new_label)+'_x'), 0)
  325. self.apply_load(new_label, Symbol('R_'+str(new_label)+'_y'), 90)
  326. self._loads.pop(label)
  327. elif self._supports[new_label] == 'roller':
  328. self._loads[new_label] = self._loads[label]
  329. for load in self._loads[label]:
  330. if load[1] == 90:
  331. load[0] -= Symbol('R_'+str(label)+'_y')
  332. if load[0] == 0:
  333. self._loads[label].remove(load)
  334. break
  335. self.apply_load(new_label, Symbol('R_'+str(new_label)+'_y'), 90)
  336. self._loads.pop(label)
  337. else:
  338. if label in self._loads:
  339. self._loads[new_label] = self._loads[label]
  340. self._loads.pop(label)
  341. for member in self._members:
  342. if self._members[member][0] == node[0]:
  343. self._members[member][0] = new_label
  344. self._nodes_occupied[(new_label, self._members[member][1])] = True
  345. self._nodes_occupied[(self._members[member][1], new_label)] = True
  346. self._nodes_occupied.pop((label, self._members[member][1]))
  347. self._nodes_occupied.pop((self._members[member][1], label))
  348. elif self._members[member][1] == node[0]:
  349. self._members[member][1] = new_label
  350. self._nodes_occupied[(self._members[member][0], new_label)] = True
  351. self._nodes_occupied[(new_label, self._members[member][0])] = True
  352. self._nodes_occupied.pop((self._members[member][0], label))
  353. self._nodes_occupied.pop((label, self._members[member][0]))
  354. def change_member_label(self, *args):
  355. """
  356. This method changes the label(s) of the specified member(s).
  357. Parameters
  358. ==========
  359. The input(s) of this method are tuple(s) of the form (label, new_label)
  360. label: String or Symbol
  361. The label of the member for which the label has
  362. to be changed.
  363. new_label: String or Symbol
  364. The new label of the member.
  365. Examples
  366. ========
  367. >>> from sympy.physics.continuum_mechanics.truss import Truss
  368. >>> t = Truss()
  369. >>> t.add_node(('A', 0, 0), ('B', 3, 0), ('D', 5, 0))
  370. >>> t.nodes
  371. [('A', 0, 0), ('B', 3, 0), ('D', 5, 0)]
  372. >>> t.change_node_label(('A', 'C'))
  373. >>> t.nodes
  374. [('C', 0, 0), ('B', 3, 0), ('D', 5, 0)]
  375. >>> t.add_member(('BC', 'B', 'C'), ('BD', 'B', 'D'))
  376. >>> t.members
  377. {'BC': ['B', 'C'], 'BD': ['B', 'D']}
  378. >>> t.change_member_label(('BC', 'BC_new'), ('BD', 'BD_new'))
  379. >>> t.members
  380. {'BC_new': ['B', 'C'], 'BD_new': ['B', 'D']}
  381. """
  382. for i in args:
  383. label = i[0]
  384. new_label = i[1]
  385. if label not in self._members:
  386. raise ValueError("No such member exists for the Truss")
  387. else:
  388. members_duplicate = list(self._members).copy()
  389. for member in members_duplicate:
  390. if member == label:
  391. self._members[new_label] = [self._members[member][0], self._members[member][1]]
  392. self._members.pop(label)
  393. self._member_lengths[new_label] = self._member_lengths[label]
  394. self._member_lengths.pop(label)
  395. self._internal_forces[new_label] = self._internal_forces[label]
  396. self._internal_forces.pop(label)
  397. def apply_load(self, *args):
  398. """
  399. This method applies external load(s) at the specified node(s).
  400. Parameters
  401. ==========
  402. The input(s) of the method are tuple(s) of the form (location, magnitude, direction).
  403. location: String or Symbol
  404. Label of the Node at which load is applied.
  405. magnitude: Sympifyable
  406. Magnitude of the load applied. It must always be positive and any changes in
  407. the direction of the load are not reflected here.
  408. direction: Sympifyable
  409. The angle, in degrees, that the load vector makes with the horizontal
  410. in the counter-clockwise direction. It takes the values 0 to 360,
  411. inclusive.
  412. Examples
  413. ========
  414. >>> from sympy.physics.continuum_mechanics.truss import Truss
  415. >>> from sympy import symbols
  416. >>> t = Truss()
  417. >>> t.add_node(('A', 0, 0), ('B', 3, 0))
  418. >>> P = symbols('P')
  419. >>> t.apply_load(('A', P, 90), ('A', P/2, 45), ('A', P/4, 90))
  420. >>> t.loads
  421. {'A': [[P, 90], [P/2, 45], [P/4, 90]]}
  422. """
  423. for i in args:
  424. location = i[0]
  425. magnitude = i[1]
  426. direction = i[2]
  427. magnitude = sympify(magnitude)
  428. direction = sympify(direction)
  429. if location not in self._node_coordinates:
  430. raise ValueError("Load must be applied at a known node")
  431. else:
  432. if location in self._loads:
  433. self._loads[location].append([magnitude, direction])
  434. else:
  435. self._loads[location] = [[magnitude, direction]]
  436. def remove_load(self, *args):
  437. """
  438. This method removes already
  439. present external load(s) at specified node(s).
  440. Parameters
  441. ==========
  442. The input(s) of this method are tuple(s) of the form (location, magnitude, direction).
  443. location: String or Symbol
  444. Label of the Node at which load is applied and is to be removed.
  445. magnitude: Sympifyable
  446. Magnitude of the load applied.
  447. direction: Sympifyable
  448. The angle, in degrees, that the load vector makes with the horizontal
  449. in the counter-clockwise direction. It takes the values 0 to 360,
  450. inclusive.
  451. Examples
  452. ========
  453. >>> from sympy.physics.continuum_mechanics.truss import Truss
  454. >>> from sympy import symbols
  455. >>> t = Truss()
  456. >>> t.add_node(('A', 0, 0), ('B', 3, 0))
  457. >>> P = symbols('P')
  458. >>> t.apply_load(('A', P, 90), ('A', P/2, 45), ('A', P/4, 90))
  459. >>> t.loads
  460. {'A': [[P, 90], [P/2, 45], [P/4, 90]]}
  461. >>> t.remove_load(('A', P/4, 90), ('A', P/2, 45))
  462. >>> t.loads
  463. {'A': [[P, 90]]}
  464. """
  465. for i in args:
  466. location = i[0]
  467. magnitude = i[1]
  468. direction = i[2]
  469. magnitude = sympify(magnitude)
  470. direction = sympify(direction)
  471. if location not in self._node_coordinates:
  472. raise ValueError("Load must be removed from a known node")
  473. else:
  474. if [magnitude, direction] not in self._loads[location]:
  475. raise ValueError("No load of this magnitude and direction has been applied at this node")
  476. else:
  477. self._loads[location].remove([magnitude, direction])
  478. if self._loads[location] == []:
  479. self._loads.pop(location)
  480. def apply_support(self, *args):
  481. """
  482. This method adds a pinned or roller support at specified node(s).
  483. Parameters
  484. ==========
  485. The input(s) of this method are of the form (location, type).
  486. location: String or Symbol
  487. Label of the Node at which support is added.
  488. type: String
  489. Type of the support being provided at the node.
  490. Examples
  491. ========
  492. >>> from sympy.physics.continuum_mechanics.truss import Truss
  493. >>> t = Truss()
  494. >>> t.add_node(('A', 0, 0), ('B', 3, 0))
  495. >>> t.apply_support(('A', 'pinned'), ('B', 'roller'))
  496. >>> t.supports
  497. {'A': 'pinned', 'B': 'roller'}
  498. """
  499. for i in args:
  500. location = i[0]
  501. type = i[1]
  502. if location not in self._node_coordinates:
  503. raise ValueError("Support must be added on a known node")
  504. else:
  505. if location not in self._supports:
  506. if type == 'pinned':
  507. self.apply_load((location, Symbol('R_'+str(location)+'_x'), 0))
  508. self.apply_load((location, Symbol('R_'+str(location)+'_y'), 90))
  509. elif type == 'roller':
  510. self.apply_load((location, Symbol('R_'+str(location)+'_y'), 90))
  511. elif self._supports[location] == 'pinned':
  512. if type == 'roller':
  513. self.remove_load((location, Symbol('R_'+str(location)+'_x'), 0))
  514. elif self._supports[location] == 'roller':
  515. if type == 'pinned':
  516. self.apply_load((location, Symbol('R_'+str(location)+'_x'), 0))
  517. self._supports[location] = type
  518. def remove_support(self, *args):
  519. """
  520. This method removes support from specified node(s.)
  521. Parameters
  522. ==========
  523. locations: String or Symbol
  524. Label of the Node(s) at which support is to be removed.
  525. Examples
  526. ========
  527. >>> from sympy.physics.continuum_mechanics.truss import Truss
  528. >>> t = Truss()
  529. >>> t.add_node(('A', 0, 0), ('B', 3, 0))
  530. >>> t.apply_support(('A', 'pinned'), ('B', 'roller'))
  531. >>> t.supports
  532. {'A': 'pinned', 'B': 'roller'}
  533. >>> t.remove_support('A','B')
  534. >>> t.supports
  535. {}
  536. """
  537. for location in args:
  538. if location not in self._node_coordinates:
  539. raise ValueError("No such node exists in the Truss")
  540. elif location not in self._supports:
  541. raise ValueError("No support has been added to the given node")
  542. else:
  543. if self._supports[location] == 'pinned':
  544. self.remove_load((location, Symbol('R_'+str(location)+'_x'), 0))
  545. self.remove_load((location, Symbol('R_'+str(location)+'_y'), 90))
  546. elif self._supports[location] == 'roller':
  547. self.remove_load((location, Symbol('R_'+str(location)+'_y'), 90))
  548. self._supports.pop(location)
  549. def solve(self):
  550. """
  551. This method solves for all reaction forces of all supports and all internal forces
  552. of all the members in the truss, provided the Truss is solvable.
  553. A Truss is solvable if the following condition is met,
  554. 2n >= r + m
  555. Where n is the number of nodes, r is the number of reaction forces, where each pinned
  556. support has 2 reaction forces and each roller has 1, and m is the number of members.
  557. The given condition is derived from the fact that a system of equations is solvable
  558. only when the number of variables is lesser than or equal to the number of equations.
  559. Equilibrium Equations in x and y directions give two equations per node giving 2n number
  560. equations. However, the truss needs to be stable as well and may be unstable if 2n > r + m.
  561. The number of variables is simply the sum of the number of reaction forces and member
  562. forces.
  563. .. note::
  564. The sign convention for the internal forces present in a member revolves around whether each
  565. force is compressive or tensile. While forming equations for each node, internal force due
  566. to a member on the node is assumed to be away from the node i.e. each force is assumed to
  567. be compressive by default. Hence, a positive value for an internal force implies the
  568. presence of compressive force in the member and a negative value implies a tensile force.
  569. Examples
  570. ========
  571. >>> from sympy.physics.continuum_mechanics.truss import Truss
  572. >>> t = Truss()
  573. >>> t.add_node(("node_1", 0, 0), ("node_2", 6, 0), ("node_3", 2, 2), ("node_4", 2, 0))
  574. >>> t.add_member(("member_1", "node_1", "node_4"), ("member_2", "node_2", "node_4"), ("member_3", "node_1", "node_3"))
  575. >>> t.add_member(("member_4", "node_2", "node_3"), ("member_5", "node_3", "node_4"))
  576. >>> t.apply_load(("node_4", 10, 270))
  577. >>> t.apply_support(("node_1", "pinned"), ("node_2", "roller"))
  578. >>> t.solve()
  579. >>> t.reaction_loads
  580. {'R_node_1_x': 0, 'R_node_1_y': 20/3, 'R_node_2_y': 10/3}
  581. >>> t.internal_forces
  582. {'member_1': 20/3, 'member_2': 20/3, 'member_3': -20*sqrt(2)/3, 'member_4': -10*sqrt(5)/3, 'member_5': 10}
  583. """
  584. count_reaction_loads = 0
  585. for node in self._nodes:
  586. if node[0] in self._supports:
  587. if self._supports[node[0]]=='pinned':
  588. count_reaction_loads += 2
  589. elif self._supports[node[0]]=='roller':
  590. count_reaction_loads += 1
  591. if 2*len(self._nodes) != len(self._members) + count_reaction_loads:
  592. raise ValueError("The given truss cannot be solved")
  593. coefficients_matrix = [[0 for i in range(2*len(self._nodes))] for j in range(2*len(self._nodes))]
  594. load_matrix = zeros(2*len(self.nodes), 1)
  595. load_matrix_row = 0
  596. for node in self._nodes:
  597. if node[0] in self._loads:
  598. for load in self._loads[node[0]]:
  599. if load[0]!=Symbol('R_'+str(node[0])+'_x') and load[0]!=Symbol('R_'+str(node[0])+'_y'):
  600. load_matrix[load_matrix_row] -= load[0]*cos(pi*load[1]/180)
  601. load_matrix[load_matrix_row + 1] -= load[0]*sin(pi*load[1]/180)
  602. load_matrix_row += 2
  603. cols = 0
  604. row = 0
  605. for node in self._nodes:
  606. if node[0] in self._supports:
  607. if self._supports[node[0]]=='pinned':
  608. coefficients_matrix[row][cols] += 1
  609. coefficients_matrix[row+1][cols+1] += 1
  610. cols += 2
  611. elif self._supports[node[0]]=='roller':
  612. coefficients_matrix[row+1][cols] += 1
  613. cols += 1
  614. row += 2
  615. for member in self._members:
  616. start = self._members[member][0]
  617. end = self._members[member][1]
  618. length = sqrt((self._node_coordinates[start][0]-self._node_coordinates[end][0])**2 + (self._node_coordinates[start][1]-self._node_coordinates[end][1])**2)
  619. start_index = self._node_labels.index(start)
  620. end_index = self._node_labels.index(end)
  621. horizontal_component_start = (self._node_coordinates[end][0]-self._node_coordinates[start][0])/length
  622. vertical_component_start = (self._node_coordinates[end][1]-self._node_coordinates[start][1])/length
  623. horizontal_component_end = (self._node_coordinates[start][0]-self._node_coordinates[end][0])/length
  624. vertical_component_end = (self._node_coordinates[start][1]-self._node_coordinates[end][1])/length
  625. coefficients_matrix[start_index*2][cols] += horizontal_component_start
  626. coefficients_matrix[start_index*2+1][cols] += vertical_component_start
  627. coefficients_matrix[end_index*2][cols] += horizontal_component_end
  628. coefficients_matrix[end_index*2+1][cols] += vertical_component_end
  629. cols += 1
  630. forces_matrix = (Matrix(coefficients_matrix)**-1)*load_matrix
  631. self._reaction_loads = {}
  632. i = 0
  633. min_load = inf
  634. for node in self._nodes:
  635. if node[0] in self._loads:
  636. for load in self._loads[node[0]]:
  637. if type(load[0]) not in [Symbol, Mul, Add]:
  638. min_load = min(min_load, load[0])
  639. for j in range(len(forces_matrix)):
  640. if type(forces_matrix[j]) not in [Symbol, Mul, Add]:
  641. if abs(forces_matrix[j]/min_load) <1E-10:
  642. forces_matrix[j] = 0
  643. for node in self._nodes:
  644. if node[0] in self._supports:
  645. if self._supports[node[0]]=='pinned':
  646. self._reaction_loads['R_'+str(node[0])+'_x'] = forces_matrix[i]
  647. self._reaction_loads['R_'+str(node[0])+'_y'] = forces_matrix[i+1]
  648. i += 2
  649. elif self._supports[node[0]]=='roller':
  650. self._reaction_loads['R_'+str(node[0])+'_y'] = forces_matrix[i]
  651. i += 1
  652. for member in self._members:
  653. self._internal_forces[member] = forces_matrix[i]
  654. i += 1
  655. return
  656. @doctest_depends_on(modules=('numpy',))
  657. def draw(self, subs_dict=None):
  658. """
  659. Returns a plot object of the Truss with all its nodes, members,
  660. supports and loads.
  661. .. note::
  662. The user must be careful while entering load values in their
  663. directions. The draw function assumes a sign convention that
  664. is used for plotting loads.
  665. Given a right-handed coordinate system with XYZ coordinates,
  666. the supports are assumed to be such that the reaction forces of a
  667. pinned support is in the +X and +Y direction while those of a
  668. roller support is in the +Y direction. For the load, the range
  669. of angles, one can input goes all the way to 360 degrees which, in the
  670. the plot is the angle that the load vector makes with the positive x-axis in the anticlockwise direction.
  671. For example, for a 90-degree angle, the load will be a vertically
  672. directed along +Y while a 270-degree angle denotes a vertical
  673. load as well but along -Y.
  674. Examples
  675. ========
  676. .. plot::
  677. :context: close-figs
  678. :format: doctest
  679. :include-source: True
  680. >>> from sympy.physics.continuum_mechanics.truss import Truss
  681. >>> import math
  682. >>> t = Truss()
  683. >>> t.add_node(("A", -4, 0), ("B", 0, 0), ("C", 4, 0), ("D", 8, 0))
  684. >>> t.add_node(("E", 6, 2/math.sqrt(3)))
  685. >>> t.add_node(("F", 2, 2*math.sqrt(3)))
  686. >>> t.add_node(("G", -2, 2/math.sqrt(3)))
  687. >>> t.add_member(("AB","A","B"), ("BC","B","C"), ("CD","C","D"))
  688. >>> t.add_member(("AG","A","G"), ("GB","G","B"), ("GF","G","F"))
  689. >>> t.add_member(("BF","B","F"), ("FC","F","C"), ("CE","C","E"))
  690. >>> t.add_member(("FE","F","E"), ("DE","D","E"))
  691. >>> t.apply_support(("A","pinned"), ("D","roller"))
  692. >>> t.apply_load(("G", 3, 90), ("E", 3, 90), ("F", 2, 90))
  693. >>> p = t.draw()
  694. >>> p # doctest: +ELLIPSIS
  695. Plot object containing:
  696. [0]: cartesian line: 1 for x over (1.0, 1.0)
  697. ...
  698. >>> p.show()
  699. """
  700. if not numpy:
  701. raise ImportError("To use this function numpy module is required")
  702. x = Symbol('x')
  703. markers = []
  704. annotations = []
  705. rectangles = []
  706. node_markers = self._draw_nodes(subs_dict)
  707. markers += node_markers
  708. member_rectangles = self._draw_members()
  709. rectangles += member_rectangles
  710. support_markers = self._draw_supports()
  711. markers += support_markers
  712. load_annotations = self._draw_loads()
  713. annotations += load_annotations
  714. xmax = -INF
  715. xmin = INF
  716. ymax = -INF
  717. ymin = INF
  718. for node in self._node_coordinates:
  719. xmax = max(xmax, self._node_coordinates[node][0])
  720. xmin = min(xmin, self._node_coordinates[node][0])
  721. ymax = max(ymax, self._node_coordinates[node][1])
  722. ymin = min(ymin, self._node_coordinates[node][1])
  723. lim = max(xmax*1.1-xmin*0.8+1, ymax*1.1-ymin*0.8+1)
  724. if lim==xmax*1.1-xmin*0.8+1:
  725. sing_plot = plot(1, (x, 1, 1), markers=markers, show=False, annotations=annotations, xlim=(xmin-0.05*lim, xmax*1.1), ylim=(xmin-0.05*lim, xmax*1.1), axis=False, rectangles=rectangles)
  726. else:
  727. sing_plot = plot(1, (x, 1, 1), markers=markers, show=False, annotations=annotations, xlim=(ymin-0.05*lim, ymax*1.1), ylim=(ymin-0.05*lim, ymax*1.1), axis=False, rectangles=rectangles)
  728. return sing_plot
  729. def _draw_nodes(self, subs_dict):
  730. node_markers = []
  731. for node in self._node_coordinates:
  732. if (type(self._node_coordinates[node][0]) in (Symbol, Quantity)):
  733. if self._node_coordinates[node][0] in subs_dict:
  734. self._node_coordinates[node][0] = subs_dict[self._node_coordinates[node][0]]
  735. else:
  736. raise ValueError("provided substituted dictionary is not adequate")
  737. elif (type(self._node_coordinates[node][0]) == Mul):
  738. objects = self._node_coordinates[node][0].as_coeff_Mul()
  739. for object in objects:
  740. if type(object) in (Symbol, Quantity):
  741. if subs_dict==None or object not in subs_dict:
  742. raise ValueError("provided substituted dictionary is not adequate")
  743. else:
  744. self._node_coordinates[node][0] /= object
  745. self._node_coordinates[node][0] *= subs_dict[object]
  746. if (type(self._node_coordinates[node][1]) in (Symbol, Quantity)):
  747. if self._node_coordinates[node][1] in subs_dict:
  748. self._node_coordinates[node][1] = subs_dict[self._node_coordinates[node][1]]
  749. else:
  750. raise ValueError("provided substituted dictionary is not adequate")
  751. elif (type(self._node_coordinates[node][1]) == Mul):
  752. objects = self._node_coordinates[node][1].as_coeff_Mul()
  753. for object in objects:
  754. if type(object) in (Symbol, Quantity):
  755. if subs_dict==None or object not in subs_dict:
  756. raise ValueError("provided substituted dictionary is not adequate")
  757. else:
  758. self._node_coordinates[node][1] /= object
  759. self._node_coordinates[node][1] *= subs_dict[object]
  760. for node in self._node_coordinates:
  761. node_markers.append(
  762. {
  763. 'args':[[self._node_coordinates[node][0]], [self._node_coordinates[node][1]]],
  764. 'marker':'o',
  765. 'markersize':5,
  766. 'color':'black'
  767. }
  768. )
  769. return node_markers
  770. def _draw_members(self):
  771. member_rectangles = []
  772. xmax = -INF
  773. xmin = INF
  774. ymax = -INF
  775. ymin = INF
  776. for node in self._node_coordinates:
  777. xmax = max(xmax, self._node_coordinates[node][0])
  778. xmin = min(xmin, self._node_coordinates[node][0])
  779. ymax = max(ymax, self._node_coordinates[node][1])
  780. ymin = min(ymin, self._node_coordinates[node][1])
  781. if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin):
  782. max_diff = 1.1*xmax-0.8*xmin
  783. else:
  784. max_diff = 1.1*ymax-0.8*ymin
  785. for member in self._members:
  786. x1 = self._node_coordinates[self._members[member][0]][0]
  787. y1 = self._node_coordinates[self._members[member][0]][1]
  788. x2 = self._node_coordinates[self._members[member][1]][0]
  789. y2 = self._node_coordinates[self._members[member][1]][1]
  790. if x2!=x1 and y2!=y1:
  791. if x2>x1:
  792. member_rectangles.append(
  793. {
  794. 'xy':(x1-0.005*max_diff*cos(pi/4+atan((y2-y1)/(x2-x1)))/2, y1-0.005*max_diff*sin(pi/4+atan((y2-y1)/(x2-x1)))/2),
  795. 'width':sqrt((x1-x2)**2+(y1-y2)**2)+0.005*max_diff/math.sqrt(2),
  796. 'height':0.005*max_diff,
  797. 'angle':180*atan((y2-y1)/(x2-x1))/pi,
  798. 'color':'brown'
  799. }
  800. )
  801. else:
  802. member_rectangles.append(
  803. {
  804. 'xy':(x2-0.005*max_diff*cos(pi/4+atan((y2-y1)/(x2-x1)))/2, y2-0.005*max_diff*sin(pi/4+atan((y2-y1)/(x2-x1)))/2),
  805. 'width':sqrt((x1-x2)**2+(y1-y2)**2)+0.005*max_diff/math.sqrt(2),
  806. 'height':0.005*max_diff,
  807. 'angle':180*atan((y2-y1)/(x2-x1))/pi,
  808. 'color':'brown'
  809. }
  810. )
  811. elif y2==y1:
  812. if x2>x1:
  813. member_rectangles.append(
  814. {
  815. 'xy':(x1-0.005*max_diff/2, y1-0.005*max_diff/2),
  816. 'width':sqrt((x1-x2)**2+(y1-y2)**2),
  817. 'height':0.005*max_diff,
  818. 'angle':90*(1-math.copysign(1, x2-x1)),
  819. 'color':'brown'
  820. }
  821. )
  822. else:
  823. member_rectangles.append(
  824. {
  825. 'xy':(x1-0.005*max_diff/2, y1-0.005*max_diff/2),
  826. 'width':sqrt((x1-x2)**2+(y1-y2)**2),
  827. 'height':-0.005*max_diff,
  828. 'angle':90*(1-math.copysign(1, x2-x1)),
  829. 'color':'brown'
  830. }
  831. )
  832. else:
  833. if y1<y2:
  834. member_rectangles.append(
  835. {
  836. 'xy':(x1-0.005*max_diff/2, y1-0.005*max_diff/2),
  837. 'width':sqrt((x1-x2)**2+(y1-y2)**2)+0.005*max_diff/2,
  838. 'height':0.005*max_diff,
  839. 'angle':90*math.copysign(1, y2-y1),
  840. 'color':'brown'
  841. }
  842. )
  843. else:
  844. member_rectangles.append(
  845. {
  846. 'xy':(x2-0.005*max_diff/2, y2-0.005*max_diff/2),
  847. 'width':-(sqrt((x1-x2)**2+(y1-y2)**2)+0.005*max_diff/2),
  848. 'height':0.005*max_diff,
  849. 'angle':90*math.copysign(1, y2-y1),
  850. 'color':'brown'
  851. }
  852. )
  853. return member_rectangles
  854. def _draw_supports(self):
  855. support_markers = []
  856. xmax = -INF
  857. xmin = INF
  858. ymax = -INF
  859. ymin = INF
  860. for node in self._node_coordinates:
  861. xmax = max(xmax, self._node_coordinates[node][0])
  862. xmin = min(xmin, self._node_coordinates[node][0])
  863. ymax = max(ymax, self._node_coordinates[node][1])
  864. ymin = min(ymin, self._node_coordinates[node][1])
  865. if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin):
  866. max_diff = 1.1*xmax-0.8*xmin
  867. else:
  868. max_diff = 1.1*ymax-0.8*ymin
  869. for node in self._supports:
  870. if self._supports[node]=='pinned':
  871. support_markers.append(
  872. {
  873. 'args':[
  874. [self._node_coordinates[node][0]],
  875. [self._node_coordinates[node][1]]
  876. ],
  877. 'marker':6,
  878. 'markersize':15,
  879. 'color':'black',
  880. 'markerfacecolor':'none'
  881. }
  882. )
  883. support_markers.append(
  884. {
  885. 'args':[
  886. [self._node_coordinates[node][0]],
  887. [self._node_coordinates[node][1]-0.035*max_diff]
  888. ],
  889. 'marker':'_',
  890. 'markersize':14,
  891. 'color':'black'
  892. }
  893. )
  894. elif self._supports[node]=='roller':
  895. support_markers.append(
  896. {
  897. 'args':[
  898. [self._node_coordinates[node][0]],
  899. [self._node_coordinates[node][1]-0.02*max_diff]
  900. ],
  901. 'marker':'o',
  902. 'markersize':11,
  903. 'color':'black',
  904. 'markerfacecolor':'none'
  905. }
  906. )
  907. support_markers.append(
  908. {
  909. 'args':[
  910. [self._node_coordinates[node][0]],
  911. [self._node_coordinates[node][1]-0.0375*max_diff]
  912. ],
  913. 'marker':'_',
  914. 'markersize':14,
  915. 'color':'black'
  916. }
  917. )
  918. return support_markers
  919. def _draw_loads(self):
  920. load_annotations = []
  921. xmax = -INF
  922. xmin = INF
  923. ymax = -INF
  924. ymin = INF
  925. for node in self._node_coordinates:
  926. xmax = max(xmax, self._node_coordinates[node][0])
  927. xmin = min(xmin, self._node_coordinates[node][0])
  928. ymax = max(ymax, self._node_coordinates[node][1])
  929. ymin = min(ymin, self._node_coordinates[node][1])
  930. if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin):
  931. max_diff = 1.1*xmax-0.8*xmin+5
  932. else:
  933. max_diff = 1.1*ymax-0.8*ymin+5
  934. for node in self._loads:
  935. for load in self._loads[node]:
  936. if load[0] in [Symbol('R_'+str(node)+'_x'), Symbol('R_'+str(node)+'_y')]:
  937. continue
  938. x = self._node_coordinates[node][0]
  939. y = self._node_coordinates[node][1]
  940. load_annotations.append(
  941. {
  942. 'text':'',
  943. 'xy':(
  944. x-math.cos(pi*load[1]/180)*(max_diff/100),
  945. y-math.sin(pi*load[1]/180)*(max_diff/100)
  946. ),
  947. 'xytext':(
  948. x-(max_diff/100+abs(xmax-xmin)+abs(ymax-ymin))*math.cos(pi*load[1]/180)/20,
  949. y-(max_diff/100+abs(xmax-xmin)+abs(ymax-ymin))*math.sin(pi*load[1]/180)/20
  950. ),
  951. 'arrowprops':{'width':1.5, 'headlength':5, 'headwidth':5, 'facecolor':'black'}
  952. }
  953. )
  954. return load_annotations