compas-cem 0.7.0__py2.py3-none-any.whl → 0.8.2__py2.py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. compas_cem/__init__.py +1 -1
  2. compas_cem/diagrams/diagram.py +16 -0
  3. compas_cem/diagrams/topology/topology.py +6 -3
  4. compas_cem/equilibrium/__init__.py +1 -0
  5. compas_cem/equilibrium/force.py +9 -3
  6. compas_cem/equilibrium/force_numpy.py +9 -3
  7. compas_cem/ghpython/components/CompasCem_ConstraintEdgeDirection/code.py +17 -0
  8. compas_cem/ghpython/components/CompasCem_ConstraintEdgeDirection/icon.png +0 -0
  9. compas_cem/ghpython/components/CompasCem_ConstraintEdgeDirection/metadata.json +35 -0
  10. compas_cem/ghpython/components/CompasCem_ConstraintNodePolyline/code.py +16 -0
  11. compas_cem/ghpython/components/CompasCem_ConstraintNodePolyline/icon.png +0 -0
  12. compas_cem/ghpython/components/CompasCem_ConstraintNodePolyline/metadata.json +36 -0
  13. compas_cem/ghpython/components/ghuser/CompasCem_ArtistColors.ghuser +0 -0
  14. compas_cem/ghpython/components/ghuser/CompasCem_ArtistForm.ghuser +0 -0
  15. compas_cem/ghpython/components/ghuser/CompasCem_ArtistTopology.ghuser +0 -0
  16. compas_cem/ghpython/components/ghuser/CompasCem_ConstrainedFormFinding.ghuser +0 -0
  17. compas_cem/ghpython/components/ghuser/CompasCem_ConstraintDeviationEdgeLength.ghuser +0 -0
  18. compas_cem/ghpython/components/ghuser/CompasCem_ConstraintEdgeDirection.ghuser +0 -0
  19. compas_cem/ghpython/components/ghuser/CompasCem_ConstraintNodeLine.ghuser +0 -0
  20. compas_cem/ghpython/components/ghuser/CompasCem_ConstraintNodePlane.ghuser +0 -0
  21. compas_cem/ghpython/components/ghuser/CompasCem_ConstraintNodePoint.ghuser +0 -0
  22. compas_cem/ghpython/components/ghuser/CompasCem_ConstraintNodePolyline.ghuser +0 -0
  23. compas_cem/ghpython/components/ghuser/CompasCem_ConstraintReactionForce.ghuser +0 -0
  24. compas_cem/ghpython/components/ghuser/CompasCem_ConstraintTrailEdgeForce.ghuser +0 -0
  25. compas_cem/ghpython/components/ghuser/CompasCem_DisassembleForm.ghuser +0 -0
  26. compas_cem/ghpython/components/ghuser/CompasCem_DisassembleTopology.ghuser +0 -0
  27. compas_cem/ghpython/components/ghuser/CompasCem_EdgeDeviation.ghuser +0 -0
  28. compas_cem/ghpython/components/ghuser/CompasCem_EdgeTrail.ghuser +0 -0
  29. compas_cem/ghpython/components/ghuser/CompasCem_FormFinding.ghuser +0 -0
  30. compas_cem/ghpython/components/ghuser/CompasCem_Info.ghuser +0 -0
  31. compas_cem/ghpython/components/ghuser/CompasCem_JSONExportDiagram.ghuser +0 -0
  32. compas_cem/ghpython/components/ghuser/CompasCem_JSONImportForm.ghuser +0 -0
  33. compas_cem/ghpython/components/ghuser/CompasCem_JSONImportTopology.ghuser +0 -0
  34. compas_cem/ghpython/components/ghuser/CompasCem_NodeLoad.ghuser +0 -0
  35. compas_cem/ghpython/components/ghuser/CompasCem_NodeSupport.ghuser +0 -0
  36. compas_cem/ghpython/components/ghuser/CompasCem_OriginNodesMove.ghuser +0 -0
  37. compas_cem/ghpython/components/ghuser/CompasCem_ParameterDeviationEdge.ghuser +0 -0
  38. compas_cem/ghpython/components/ghuser/CompasCem_ParameterNodeLoadX.ghuser +0 -0
  39. compas_cem/ghpython/components/ghuser/CompasCem_ParameterNodeLoadY.ghuser +0 -0
  40. compas_cem/ghpython/components/ghuser/CompasCem_ParameterNodeLoadZ.ghuser +0 -0
  41. compas_cem/ghpython/components/ghuser/CompasCem_ParameterNodeOriginX.ghuser +0 -0
  42. compas_cem/ghpython/components/ghuser/CompasCem_ParameterNodeOriginY.ghuser +0 -0
  43. compas_cem/ghpython/components/ghuser/CompasCem_ParameterNodeOriginZ.ghuser +0 -0
  44. compas_cem/ghpython/components/ghuser/CompasCem_ParameterTrailEdge.ghuser +0 -0
  45. compas_cem/ghpython/components/ghuser/CompasCem_Proxy.ghuser +0 -0
  46. compas_cem/ghpython/components/ghuser/CompasCem_ResultsEdges.ghuser +0 -0
  47. compas_cem/ghpython/components/ghuser/CompasCem_ResultsSupportNodes.ghuser +0 -0
  48. compas_cem/ghpython/components/ghuser/CompasCem_SearchEdgeKey.ghuser +0 -0
  49. compas_cem/ghpython/components/ghuser/CompasCem_SearchNodeKey.ghuser +0 -0
  50. compas_cem/ghpython/components/ghuser/CompasCem_TopologyDiagram.ghuser +0 -0
  51. compas_cem/ghpython/components/ghuser/CompasCem_TrailsShift.ghuser +0 -0
  52. compas_cem/optimization/constraints/__init__.py +2 -0
  53. compas_cem/optimization/constraints/direction.py +191 -0
  54. compas_cem/optimization/constraints/polyline.py +149 -0
  55. compas_cem/optimization/optimizer.py +2 -2
  56. compas_cem/optimization/parameters/load.py +4 -2
  57. compas_cem/viewers/diagramobject.py +1 -0
  58. compas_cem-0.8.2.dist-info/AUTHORS.rst +41 -0
  59. {compas_cem-0.7.0.dist-info → compas_cem-0.8.2.dist-info}/METADATA +183 -189
  60. {compas_cem-0.7.0.dist-info → compas_cem-0.8.2.dist-info}/RECORD +63 -54
  61. {compas_cem-0.7.0.dist-info → compas_cem-0.8.2.dist-info}/WHEEL +1 -1
  62. compas_cem-0.7.0.dist-info/AUTHORS.rst +0 -41
  63. compas_cem-0.7.0.dist-info/entry_points.txt +0 -3
  64. {compas_cem-0.7.0.dist-info → compas_cem-0.8.2.dist-info}/LICENSE +0 -0
  65. {compas_cem-0.7.0.dist-info → compas_cem-0.8.2.dist-info}/top_level.txt +0 -0
compas_cem/__init__.py CHANGED
@@ -27,7 +27,7 @@ __author__ = ["Rafael Pastrana"]
27
27
  __copyright__ = "Copyright 2020 - Princeton University"
28
28
  __license__ = "MIT License"
29
29
  __email__ = "arpj@princeton.edu"
30
- __version__ = "0.7.0"
30
+ __version__ = "0.8.2"
31
31
 
32
32
 
33
33
  # Directories
@@ -255,6 +255,22 @@ class Diagram(Data, NodeMixins, EdgeMixins, Network):
255
255
  """
256
256
  return self.edge_attribute(key=edge, name="length")
257
257
 
258
+ def edge_plane(self, edge):
259
+ """
260
+ Gets the projection plane at an edge.
261
+
262
+ Parameters
263
+ ----------
264
+ edge : ``tuple``
265
+ The u, v edge key.
266
+
267
+ Return
268
+ ------
269
+ plane : ``tuple``
270
+ The projection plane of the edge.
271
+ """
272
+ return self.edge_attribute(key=edge, name="plane")
273
+
258
274
  # ==============================================================================
259
275
  # Magic methods
260
276
  # ==============================================================================
@@ -737,7 +737,8 @@ class TopologyDiagram(Diagram, MeshMixins):
737
737
  flag : ``bool``
738
738
  ``True``if the edge is in an auxiliary trail. ``False`` otherwise.
739
739
  """
740
- if edge in set(self.auxiliary_trails()):
740
+ aux_trails = set([tuple(edge) for edge in self.auxiliary_trails()])
741
+ if edge in set(aux_trails):
741
742
  return True
742
743
  return False
743
744
 
@@ -910,7 +911,7 @@ class TopologyDiagram(Diagram, MeshMixins):
910
911
 
911
912
  def trail_sequences(self, key):
912
913
  """
913
- Create a mapping between the sequences in the diagram and the nodes in the trail.
914
+ Create a mapping between topological sequences and the nodes in a trail.
914
915
 
915
916
  Parameters
916
917
  ----------
@@ -930,7 +931,9 @@ class TopologyDiagram(Diagram, MeshMixins):
930
931
 
931
932
  def trails_sequences(self):
932
933
  """
933
- Creates a mapping between the nodes in all the trails and the available sequences.
934
+ Creates a mapping of mappings between the nodes of the trails and the sequences.
935
+
936
+ The mapping has the form ``{trail_key: {sequence: node}}``.
934
937
 
935
938
  Returns
936
939
  -------
@@ -29,4 +29,5 @@ import compas
29
29
  if not compas.IPY:
30
30
  from .force_numpy import * # noqa F403
31
31
 
32
+
32
33
  __all__ = [name for name in dir() if not name.startswith('_')]
@@ -69,6 +69,7 @@ def equilibrium_state(topology, kmax=None, tmax=100, eta=1e-6, verbose=False, ca
69
69
  # create data containers that describe equilibrium state
70
70
  reaction_forces = {}
71
71
  trail_forces = {}
72
+ trail_directions = {}
72
73
  residual_vectors = {node: topology.reaction_force(node) for node in topology.nodes()}
73
74
  node_xyz = {node: topology.node_coordinates(node) for node in topology.nodes()}
74
75
 
@@ -84,7 +85,7 @@ def equilibrium_state(topology, kmax=None, tmax=100, eta=1e-6, verbose=False, ca
84
85
  # store last positions for residual
85
86
  last_xyz = {k: v for k, v in node_xyz.items()}
86
87
 
87
- for k in range(klast + 1): # sequences
88
+ for k in range(topology.number_of_sequences()): # sequences
88
89
 
89
90
  for key, trail in topology.trails(keys=True):
90
91
 
@@ -144,12 +145,16 @@ def equilibrium_state(topology, kmax=None, tmax=100, eta=1e-6, verbose=False, ca
144
145
  length = plength
145
146
 
146
147
  # store next node position
147
- next_pos = add_vectors(pos, scale_vector(normalize_vector(rvec), length))
148
+ nrvec = normalize_vector(rvec)
149
+ next_pos = add_vectors(pos, scale_vector(nrvec, length))
148
150
  node_xyz[next_node] = next_pos
149
151
 
150
152
  # store trail force
151
153
  trail_forces[edge] = copysign(length_vector(rvec), length)
152
154
 
155
+ # store trail direction
156
+ trail_directions[edge] = nrvec
157
+
153
158
  # store residual vector
154
159
  residual_vectors[next_node] = rvec
155
160
 
@@ -185,11 +190,12 @@ def equilibrium_state(topology, kmax=None, tmax=100, eta=1e-6, verbose=False, ca
185
190
  eq_state["node_xyz"] = node_xyz
186
191
  eq_state["trail_forces"] = trail_forces
187
192
  eq_state["reaction_forces"] = reaction_forces
193
+ eq_state["trail_directions"] = trail_directions
188
194
 
189
195
  return eq_state
190
196
 
191
197
 
192
- def form_update(form, node_xyz, trail_forces, reaction_forces):
198
+ def form_update(form, node_xyz, trail_forces, reaction_forces, **kwargs):
193
199
  """
194
200
  Update the node and edge attributes of a form after equilibrating it.
195
201
  """
@@ -57,7 +57,7 @@ def equilibrium_state_numpy(topology, tmax=100, eta=1e-6, verbose=False, callbac
57
57
 
58
58
  # output, mutable
59
59
  edge_forces = {e: np.array(topology.edge_force(e)) for e in topology.edges()}
60
- edge_lengths = {e: np.array(topology.edge_attribute(e, "length")) for e in topology.edges()}
60
+ edge_lengths = {e: np.array(topology.edge_length_2(e)) for e in topology.edges()}
61
61
 
62
62
  # input, immutable
63
63
  # numpy
@@ -70,7 +70,7 @@ def equilibrium_state_numpy(topology, tmax=100, eta=1e-6, verbose=False, callbac
70
70
  # edge planes
71
71
  edge_planes = {}
72
72
  for edge in topology.trail_edges():
73
- plane = topology.edge_attribute(edge, "plane")
73
+ plane = topology.edge_plane(edge)
74
74
  if not plane:
75
75
  continue
76
76
  plane = [np.array(vector) for vector in plane]
@@ -82,6 +82,7 @@ def equilibrium_state_numpy(topology, tmax=100, eta=1e-6, verbose=False, callbac
82
82
  # output
83
83
  reaction_forces = {}
84
84
  trail_forces = {}
85
+ trail_directions = {}
85
86
 
86
87
  for t in range(tmax): # max iterations
87
88
 
@@ -152,11 +153,15 @@ def equilibrium_state_numpy(topology, tmax=100, eta=1e-6, verbose=False, callbac
152
153
  # compute trail force
153
154
  trail_force = length_vector_numpy(rvec) # always positive
154
155
 
156
+ # compute trail direction by normalizing residual vector
155
157
  # NOTE: to avoid NaNs, do not normalize residual vector if it is zero length
156
158
  nrvec = rvec / trail_force
157
159
  if np.isnan(length_vector_numpy(nrvec)):
158
160
  nrvec = rvec
159
161
 
162
+ # store trail direction
163
+ trail_directions[edge] = nrvec
164
+
160
165
  # store next node position
161
166
  next_pos = pos + length * nrvec
162
167
  node_xyz[next_node] = next_pos
@@ -203,13 +208,14 @@ def equilibrium_state_numpy(topology, tmax=100, eta=1e-6, verbose=False, callbac
203
208
  eq_state = {}
204
209
  eq_state["node_xyz"] = node_xyz
205
210
  eq_state["trail_forces"] = trail_forces
211
+ eq_state["trail_directions"] = trail_directions
206
212
  eq_state["reaction_forces"] = reaction_forces
207
213
 
208
214
  # return node_xyz, trail_forces, reaction_forces
209
215
  return eq_state
210
216
 
211
217
 
212
- def form_update(form, node_xyz, trail_forces, reaction_forces):
218
+ def form_update(form, node_xyz, trail_forces, reaction_forces, **kwargs):
213
219
  """
214
220
  Update the node and edge attributes of a form after equilibrating it.
215
221
  """
@@ -0,0 +1,17 @@
1
+ """
2
+ Align the direction of a trail or a deviation edge with a target vector.
3
+ """
4
+
5
+ from ghpythonlib.componentbase import executingcomponent as component
6
+
7
+ from compas_cem.optimization import EdgeDirectionConstraint
8
+ from compas_rhino.geometry import RhinoVector
9
+
10
+
11
+ class EdgeDirectionConstraintComponent(component):
12
+ def RunScript(self, edge_key, vector, weight):
13
+ weight = weight or 1.0
14
+ if not edge_key or not vector:
15
+ return
16
+ vector = RhinoVector.from_geometry(vector).to_compas()
17
+ return EdgeDirectionConstraint(edge_key, vector, weight)
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "Edge Direction Constraint",
3
+ "nickname": "EdgeDirectionConstraint",
4
+ "category": "COMPAS CEM",
5
+ "subcategory": "06_Optimization",
6
+ "description": "Align the direction of a trail or a deviation edge with a target vector.",
7
+ "exposure": 4,
8
+
9
+ "ghpython": {
10
+ "isAdvancedMode": true,
11
+ "inputParameters": [
12
+ {
13
+ "name": "edge_key",
14
+ "description": "The key of a COMPAS CEM trail or deviation edge."
15
+ },
16
+ {
17
+ "name": "vector",
18
+ "description": "The target vector.",
19
+ "typeHintID": "vector"
20
+ },
21
+ {
22
+ "name": "weight",
23
+ "description": "The weight of the constraint in the optimization problem. Defaults to one.",
24
+ "typeHintID": "float"
25
+ }
26
+ ],
27
+ "outputParameters": [
28
+ {
29
+ "name": "constraint",
30
+ "description": "A COMPAS CEM edge direction constraint.",
31
+ "optional": false
32
+ }
33
+ ]
34
+ }
35
+ }
@@ -0,0 +1,16 @@
1
+ """
2
+ Pull the position of a node to a target polyline.
3
+ """
4
+ from ghpythonlib.componentbase import executingcomponent as component
5
+
6
+ from compas_cem.optimization import PolylineConstraint
7
+ from compas_rhino.geometry import RhinoPolyline
8
+
9
+
10
+ class PolylineConstraintComponent(component):
11
+ def RunScript(self, node_key, polyline, weight):
12
+ weight = weight or 1.0
13
+ if node_key is None or not polyline:
14
+ return
15
+ polyline = RhinoPolyline.from_geometry(polyline).to_compas()
16
+ return PolylineConstraint(node_key, polyline, weight)
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "Polyline Constraint",
3
+ "nickname": "PolylineConstraint",
4
+ "category": "COMPAS CEM",
5
+ "subcategory": "06_Optimization",
6
+ "description": "Pull the position of a node to a target polyline.",
7
+ "exposure": 4,
8
+
9
+ "ghpython": {
10
+ "isAdvancedMode": true,
11
+ "inputParameters": [
12
+ {
13
+ "name": "node_key",
14
+ "description": "The key of a COMPAS CEM node.",
15
+ "typeHintID": "int"
16
+ },
17
+ {
18
+ "name": "polyline",
19
+ "description": "The target polyline.",
20
+ "typeHintID": "polyline"
21
+ },
22
+ {
23
+ "name": "weight",
24
+ "description": "The weight of the constraint in the optimization problem. Defaults to one.",
25
+ "typeHintID": "float"
26
+ }
27
+ ],
28
+ "outputParameters": [
29
+ {
30
+ "name": "constraint",
31
+ "description": "A COMPAS CEM node polyline constraint.",
32
+ "optional": false
33
+ }
34
+ ]
35
+ }
36
+ }
@@ -34,6 +34,8 @@ from .plane import * # noqa F403
34
34
  from .line import * # noqa F403
35
35
  from .force import * # noqa F403
36
36
  from .length import * # noqa F403
37
+ from .direction import * # noqa F403
38
+ from .polyline import * # noqa F403
37
39
 
38
40
  # import compas
39
41
  # if not compas.IPY:
@@ -0,0 +1,191 @@
1
+ from compas.geometry import dot_vectors
2
+ from compas.geometry import normalize_vector
3
+ from compas.geometry import subtract_vectors
4
+ from compas.geometry import length_vector_sqrd
5
+ from compas.geometry import scale_vector
6
+
7
+ from compas_cem.optimization.constraints import VectorConstraint
8
+
9
+
10
+ __all__ = ["EdgeDirectionConstraint"]
11
+
12
+
13
+ class EdgeDirectionConstraint(VectorConstraint):
14
+ """
15
+ Align the direction of a trail or a deviation edge with a target vector.
16
+
17
+ Note that the ordering of the nodes of the edge to constrain matters.
18
+ The reference direction of the edge is a vector pointing from its starting node (u),
19
+ towards its end node (v).
20
+ """
21
+ def __init__(self, edge=None, vector=None, weight=1.0):
22
+ super(EdgeDirectionConstraint, self).__init__(edge, vector, weight)
23
+
24
+ def target(self, reference):
25
+ """
26
+ The target, unit-length vector.
27
+ """
28
+ target = normalize_vector(self._target)
29
+ return self._aligned_vector(target, reference)
30
+
31
+ def reference(self, data):
32
+ """
33
+ The unitized edge direction.
34
+ """
35
+ vector = self._vector_two_points(self.key(), data)
36
+ return self._unitized_vector(vector)
37
+
38
+ @staticmethod
39
+ def _vector_two_points(edge, data):
40
+ """
41
+ Create a vector from the XYZ coordinates of two nodes.
42
+ """
43
+ u, v = edge
44
+ return subtract_vectors(data["node_xyz"][v], data["node_xyz"][u])
45
+
46
+ @staticmethod
47
+ def _unitized_vector(vector):
48
+ """
49
+ Scale a copy of a vector such that its length is equal to one.
50
+ """
51
+ return scale_vector(vector, 1.0 / (length_vector_sqrd(vector) ** 0.5))
52
+
53
+ @staticmethod
54
+ def _aligned_vector(vector, vector_ref):
55
+ """
56
+ Align a vector to another such that their dot product is non-negative.
57
+ """
58
+ if dot_vectors(vector, vector_ref) < 0.0:
59
+ return scale_vector(vector, -1.0)
60
+ return vector
61
+
62
+
63
+ if __name__ == "__main__":
64
+
65
+ from math import fabs
66
+
67
+ from compas.geometry import Translation
68
+
69
+ from compas_cem.diagrams import TopologyDiagram
70
+
71
+ from compas_cem.elements import Node
72
+ from compas_cem.elements import TrailEdge
73
+ from compas_cem.elements import DeviationEdge
74
+
75
+ from compas_cem.loads import NodeLoad
76
+ from compas_cem.supports import NodeSupport
77
+
78
+ from compas_cem.equilibrium import static_equilibrium
79
+
80
+ from compas_cem.optimization import Optimizer
81
+ from compas_cem.optimization import DeviationEdgeParameter
82
+
83
+ from compas_cem.plotters import Plotter
84
+
85
+ # ------------------------------------------------------------------------------
86
+ # Instantiate a topology diagram
87
+ # ------------------------------------------------------------------------------
88
+
89
+ topology = TopologyDiagram()
90
+
91
+ # ------------------------------------------------------------------------------
92
+ # Add nodes
93
+ # ------------------------------------------------------------------------------
94
+
95
+ topology.add_node(Node(0, [0.0, 0.0, 0.0]))
96
+ topology.add_node(Node(1, [1.0, 0.0, 0.0]))
97
+ topology.add_node(Node(2, [2.5, 0.0, 0.0]))
98
+ topology.add_node(Node(3, [3.5, 0.0, 0.0]))
99
+
100
+ # ------------------------------------------------------------------------------
101
+ # Add edges
102
+ # ------------------------------------------------------------------------------
103
+
104
+ topology.add_edge(TrailEdge(1, 0, length=-1.0))
105
+ topology.add_edge(DeviationEdge(1, 2, force=-1.0))
106
+ topology.add_edge(TrailEdge(2, 3, length=-1.0))
107
+
108
+ # ------------------------------------------------------------------------------
109
+ # Add supports
110
+ # ------------------------------------------------------------------------------
111
+
112
+ topology.add_support(NodeSupport(0))
113
+ topology.add_support(NodeSupport(3))
114
+
115
+ # ------------------------------------------------------------------------------
116
+ # Add loads
117
+ # ------------------------------------------------------------------------------
118
+
119
+ topology.add_load(NodeLoad(1, [0.0, -1.0, 0.0]))
120
+ topology.add_load(NodeLoad(2, [0.0, -1.0, 0.0]))
121
+
122
+ # ------------------------------------------------------------------------------
123
+ # Build trails automatically
124
+ # ------------------------------------------------------------------------------
125
+
126
+ topology.build_trails()
127
+
128
+ # ------------------------------------------------------------------------------
129
+ # Compute a state of static equilibrium
130
+ # ------------------------------------------------------------------------------
131
+
132
+ form = static_equilibrium(topology, eta=1e-6, tmax=100, verbose=True)
133
+
134
+ # ------------------------------------------------------------------------------
135
+ # Initialize optimizer
136
+ # ------------------------------------------------------------------------------
137
+
138
+ opt = Optimizer()
139
+
140
+ # ------------------------------------------------------------------------------
141
+ # Define constraints
142
+ # ------------------------------------------------------------------------------
143
+
144
+ vector = [0.0, -2.0, 0.0]
145
+ for edge in topology.trail_edges():
146
+ constraint = EdgeDirectionConstraint(edge, vector)
147
+ opt.add_constraint(constraint)
148
+
149
+ # ------------------------------------------------------------------------------
150
+ # Define optimization parameters
151
+ # ------------------------------------------------------------------------------
152
+
153
+ for edge in topology.deviation_edges():
154
+ opt.add_parameter(DeviationEdgeParameter(edge, bound_low=10.0, bound_up=10.0))
155
+
156
+ # ------------------------------------------------------------------------------
157
+ # Optimization
158
+ # ------------------------------------------------------------------------------
159
+
160
+ form_opt = opt.solve(topology=topology,
161
+ algorithm="SLSQP",
162
+ verbose=True)
163
+
164
+ # ------------------------------------------------------------------------------
165
+ # Test
166
+ # ------------------------------------------------------------------------------
167
+
168
+ for edge in topology.deviation_edges():
169
+ force = fabs(topology.edge_force(edge))
170
+ assert force <= 1e-3
171
+
172
+ # ------------------------------------------------------------------------------
173
+ # Plot results
174
+ # ------------------------------------------------------------------------------
175
+
176
+ plotter = Plotter()
177
+
178
+ # add topology diagram to scene
179
+ plotter.add(topology, show_nodetext=True, nodesize=0.2)
180
+
181
+ # add shifted form diagram to the scene
182
+ form = form.transformed(Translation.from_vector([0.0, -1.0, 0.0]))
183
+ plotter.add(form, nodesize=0.2, show_edgetext=True, edgetext="force")
184
+
185
+ # add shifted form diagram to the scene
186
+ form_opt = form_opt.transformed(Translation.from_vector([0.0, -2.5, 0.0]))
187
+ plotter.add(form_opt, nodesize=0.2, show_edgetext=True, edgetext="force")
188
+
189
+ # show plotter contents
190
+ plotter.zoom_extents()
191
+ plotter.show()