├── output ├── .gitignore └── frames │ └── .gitignore ├── .gitignore ├── imgs ├── demo.gif ├── strength.png ├── double_anchor.png ├── single_anchor.png ├── overconstrained.png └── underconstrained.png ├── requirements.txt └── readme.md /output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /output/frames/.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .python-version 3 | __pycache__ 4 | *.gif 5 | *.png 6 | -------------------------------------------------------------------------------- /imgs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eschluntz/PytorchBridge/HEAD/imgs/demo.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | torch==2.1.2 2 | imageio==2.33.1 3 | numpy==1.26.2 4 | matplotlib==3.8.2 -------------------------------------------------------------------------------- /imgs/strength.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eschluntz/PytorchBridge/HEAD/imgs/strength.png -------------------------------------------------------------------------------- /imgs/double_anchor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eschluntz/PytorchBridge/HEAD/imgs/double_anchor.png -------------------------------------------------------------------------------- /imgs/single_anchor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eschluntz/PytorchBridge/HEAD/imgs/single_anchor.png -------------------------------------------------------------------------------- /imgs/overconstrained.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eschluntz/PytorchBridge/HEAD/imgs/overconstrained.png -------------------------------------------------------------------------------- /imgs/underconstrained.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eschluntz/PytorchBridge/HEAD/imgs/underconstrained.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Torch Bridge: Optimizing Bridge trusses using Pytorch Autograd 2 | ![img](imgs/demo.gif) 3 | 4 | You can use Pytorch for more than just Neural Networks - its autograd is super powerful for any problem where you need gradients (and are too lazy to calculate them yourself...)! 5 | 6 | This notebook first creates a Truss object, then constructs and solves a system of equations to find the force in each edge that balances each node. Finally, we sum up the "cost" of each of those edges, taking into account that compression strength goes down for longer distances. 7 | 8 | Because we can write out in closed form the "cost" of a truss, we can use autograd to calculate its gradients, 9 | then use an optimizer to move the nodes to lower the cost! 10 | 11 | ## Representing the Truss 12 | The truss is stored as a list of node `X,Y` coordinates, each of which is stored as a separate `torch.Tensor` so that some of their gradients can be frozen. Each edge is stored as a pair of node indexes. 13 | 14 | Loads: An arbitrary number of loads can be added. Each one is a 2d vector force acting on a node. Stored as `[(node_index, x_load, y_load),]` and shown with green arrows. 15 | 16 | Anchors: Certain nodes are fixed in one or more axis, taking up any needed force. Stored as `[(node_index, x_set, y_set),]` and shown with green boxes. 17 | 18 | Forces: for each edge, I need to store a force acting in the edge. `Positive = compression, negative = tension`. 19 | That force applies equal and opposite to the two attached nodes, split among x,y components. 20 | 21 | ## Force calculation algorithm 22 | Calculating all forces throughout the truss is done by solving a system of equations such that each node 23 | reaches equilibrium. 24 | For each node: the x and y components of all incoming force vectors exactly matches the load: 25 | 26 | Take a equilateral triangle with a load of 10 applied downward at the top, and 5 applied up at each bottom 27 | 28 | ``` 29 | A 30 | / \ 31 | F1 F2 32 | / \ 33 | B---F3--C 34 | ``` 35 | 36 | (A positive force is upward or rightward. Angles are relative to the horizontal) 37 | 38 | Each node creates two equations, one for its X and one for its Y equilibrium: 39 | 40 | ``` 41 | Ax: F1*cos(th_ba) + F2*cos(th_ca) + 0 == -load_Ax (==0) 42 | Ay: F1*sin(th_ba) + F2*sin(th_ca) + 0 == -load_Ay (==-10) 43 | Bx: F1*cos(th_ab) + F2*0 + F3*cos(th_cb) == -load_Bx (==0) 44 | By: F1*sin(th_ab) + F2*0 + F3*sin(th_cb) == -load_By (==5) 45 | Cx: F1*0 + F2*cos(th_ac) + F3*cos(th_bc) == -load_Cx (==0) 46 | Cy: F1*0 + F2*sin(th_ac) + F3*sin(th_bc) == -load_Cy (==5) 47 | ``` 48 | 49 | This can be written in matrix form as: 50 | 51 | ``` 52 | cos(th_ba), cos(th_ca), 0 -load_Ax (==0) 53 | sin(th_ba), sin(th_ca), 0 F1 -load_Ay (==-10) 54 | cos(th_ab), 0 , cos(th_cb) @ F2 == -load_Bx (==0) 55 | sin(th_ab), 0 , sin(th_cb) F3 -load_By (==5) 56 | 0 , cos(th_ac), cos(th_bc) -load_Cx (==0) 57 | 0 , sin(th_ac), sin(th_bc) 58 | 59 | A @ F = L 60 | ``` 61 | 62 | Where F is a vector of the forces in each beam, 63 | A is a matrix constructed from the x,y force equations for each node, 64 | and L is a vector of Loads. A will be `2N x M` (x,y for each node) and M columns (one for each beam). 65 | 66 | NOTE: those cos/sin(theta) will be simplified to the X/Y component: e.g. 67 | `cos(theta) == dx/sqrt(dx^2 + dy^2)` 68 | 69 | ### What about Anchor Points? 70 | At an anchor point, any resultant force is allowed, because it is supplied by the anchor to the ground. 71 | A full anchor allows both X and Y forces to be non-zero, and a 72 | partial anchor allows a resulant force in just one dimension. 73 | 74 | ![img](imgs/single_anchor.png) 75 | 76 | ![img](imgs/double_anchor.png) 77 | 78 | Notice that the triangle with two full anchors does not need any tension in the bottom beam. 79 | 80 | In order to account for anchor points, their nodes can simply 81 | be removed from the system of equations! A partial X or Y anchor can 82 | just have one of its two lines removed from the system. 83 | 84 | ### Solving the Equation 85 | 86 | Now we need to solve `A @ F = L` for our force vector F with `torch.linalg.lstsq`! 87 | 88 | But will there always be a solution? 89 | 90 | #### Underconstrained 91 | If the system is underconstrained (Imagine a floating node with just a single edge), that means there will be some 92 | nodes that cannot reach equilibrium and will have a net force. 93 | Luckily, that is detected by the `lstsq` call, which will have a 94 | high residual! This will raise a `ValueError` when solving. 95 | 96 | ![img](imgs/underconstrained.png) 97 | 98 | ### Overconstrained systems 99 | 100 | In an overconstrained system, there are many possible solutions of how force 101 | could be distributed among the beams. In reality, force would be distributed based on the stiffness and displacement of each beam. See [this link](https://josecarlosbellido.files.wordpress.com/2016/04/aranda-bellido-optruss.pdf) for an example of solving this system more exactly. 102 | 103 | Here, I'm simply letting the `Least Squares` solver minimize the L2 norm of the forces, which produces a decent result balancing the force between all the possible beams. 104 | 105 | ![img](imgs/overconstrained.png) 106 | 107 | ## Optimizing the Truss 108 | 109 | Here's where the whole point of using `pytorch` comes in! Now that we've solved the force in each beam of the truss, we can calculate a total cost aka "loss" ;) and then move the nodes based on their gradient with respect to the total cost. 110 | 111 | Tensile strength remains constant with length, but Compression strength gets weaker for longer beams because they risk bending and buckling. 112 | 113 | Tension: `cost = length * force / tensile_strength` 114 | 115 | Compression: `cost = length * force / compression_strength(length)` 116 | 117 | We can set `tensile_strength = 1` and use an approximation `compression_strength = 3 / (length + 3)` to get: 118 | 119 | Compression: `cost = length * force * (length + 3) / 3` 120 | 121 | ![img](imgs/strength.png) 122 | 123 | Now we can sum up the costs of all the beams in compression and tension, backprop the gradients, and adjust the truss nodes' positions: 124 | 125 | ``` 126 | cost = self.get_cost() 127 | optimizer.zero_grad() 128 | cost.backward() 129 | optimizer.step() # parameters are all of the non-frozen node position tensors 130 | ``` 131 | 132 | # Future work 133 | 134 | Assigning forces: The biggest problem with this system is that using `lstsq` to assign forces minimizes the L2 norm of the forces when there are multiple possible solutions, which isn't actually the "cheapest" possible solution! In theory, the cheapest truss could contain several very high forces, and many very small forces (imagine the pillars of a supension bridge.) More solving / assigning forces while remaining differentiable seems tricky. 135 | 136 | Dynamically change nodes and topology: It would be interesting to let the optimization dynamically add and remove nodes and edges in addition to just moving the nodes! I could imagine deleting any edges/nodes where the force goes to zero, and adding new nodes in the middle of high-force beams! It would also be fun to randomly initialize a truss and watch what it develops into. 137 | 138 | # TODOs 139 | 140 | - [x] basic class 141 | - [x] add and render loads 142 | - [x] store and render forces 143 | - [x] force solve 144 | - [x] handle anchor points 145 | - [x] render 146 | - [x] remove from matrix equation 147 | - [x] write test 148 | - [x] print / output forces at anchors 149 | - [x] think about over and under specified graphs? check residuals? 150 | - [x] optimize! 151 | - [x] write "loss" focuntion 152 | - [x] convert variables into tensors 153 | - [x] switch to torch.lstqr 154 | - [x] torch lstsq is giving different outputs??? 155 | - [x] freeze node locations that are anchors or loads 156 | - [x] fix draw 157 | - [x] fix tests 158 | - [x] fix 159 | - [x] create functions / scripts 160 | - [ ] improve 161 | - [x] try different optimization? - momentum is bad 162 | - [ ] gradient clipping? handling nodes too close? 163 | - [ ] delete members that go to zero force? 164 | - [ ] "regularization" to keep nodes far from each other? --------------------------------------------------------------------------------