Source code for gastop.truss

"""truss.py
This file is a part of GASTOp
Authors: Amlan Sinha, Cristian Lacey, Daniel Shaw, Paul Kaneelil, Rory Conlin, Susan Redmond
Licensed under GNU GPLv3.
This module implements the Truss class.

"""
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np


[docs]class Truss(): """Implements the Truss object, which is the fundamental object/data type in GASTOp. Each truss is defined by a collection of nodes (points in x,y,z space), edges (connections between nodes), and properties (material and geometric properties of the connections between nodes). A truss can also have assigned attributes such as factor of safety, deflections, mass, cost, or fitness score. These attributes are calculated based on the nodes, edges, and properties. """
[docs] def __init__(self, user_spec_nodes, rand_nodes, edges, properties, fos=None, deflection=None, mass=None, interference=None, cost=None, num_joints=None, fitness_score=None): """Creates a Truss object Args: user_spec_nodes (ndarray): Array of user specified nodes, such as where loads are applied or where the structure is supported. Array shape should be nx3, where n is the number of specified nodes. Each row should contain the x,y,z coordinates of a node. rand_nodes (ndarray): Randomly generated nodes. No loads or supports should be assigned to random nodes, as their position may change. Array shape should be mx3 where m is the number of random nodes. Each row should contain the x,y,z coordinates of a node. edges (ndarray): Array of connections between nodes. Array shape should be kx2, where k is the number of connections or beams in the structure. Each row should be 2 integers, the first being number of the starting node and the second being the ending node. A value of -1 indicates no connection, and will be ignored. properties (ndarray): Array of indices for beam properties. Array shape should be a 1d array of length k, where k is the number of connections or beams in the structure. Each entry should be an integer index into the properties dictionary, with values between [0, number of beam types]. fos (ndarray): Array of factor of safety values. Default None. deflection (ndarray): Array of node deflections under load, in meters. Default None. mass (float): Mass of the structure, in kilograms. Default None. interference (float): Total length of members passing through user specified areas. Default None. cost (float): Cost of the structure in dollars. Default None. num_joints (int): Number of connections between members. Default None. fitness_score (float): Fitness score of the truss. Default None. Returns: Truss object. """ self.user_spec_nodes = user_spec_nodes self.rand_nodes = rand_nodes self.edges = edges self.properties = properties self.fos = fos self.deflection = deflection self.mass = mass self.interference = interference self.cost = cost self.num_joints = num_joints self.fitness_score = fitness_score
# def sort(self): # """Not implemented yet. # TODO: method to sort or hash truss object so that two trusses can be # meaningfully compared. # """ # pass
[docs] def mark_duplicates(self): """Checks truss for duplicate edges or self connected nodes and marks them. Any edge that connects a node to itself, or any duplicate edges are changed to -1. Args: None Returns: None """ orig_num_edges = self.edges.shape[0] # mark self connected edges self.edges[self.edges[:, 0] == self.edges[:, 1]] = -1 # mark duplicate edges self.edges.sort() unique, idx = np.unique(self.edges, axis=0, return_index=True) duplicates = np.setdiff1d(range(orig_num_edges), idx) self.edges[duplicates, :] = -1 return
[docs] def cleaned_params(self): """Returns cleaned copies of node, edge, and property arrays. Args: None Returns: 3-element tuple containing: - **nodes** *(ndarray)*: Concatenation of user_spec_nodes and rand_nodes. - **edges** *(ndarray)*: Edges array after removing rows with -1 values. - **properties** *(ndarray)*: Properties corresponding to remaining edges. """ # make local copies of arrays in case something breaks nodes = np.concatenate((self.user_spec_nodes, self.rand_nodes)) edges = self.edges.copy() properties = self.properties.copy() # remove self connected edges and duplicate members properties = properties[(edges[:, 0]) >= 0] edges = edges[(edges[:, 0]) >= 0] properties = properties[(edges[:, 1]) >= 0] edges = edges[(edges[:, 1]) >= 0] return nodes, edges.astype(int), properties.astype(int)
[docs] def __str__(self): """Prints the truss to the terminal as a formatted array. Prints node numbers and locations, edge numbers and connections, and beam material property ID's If deflections, mass, fos, or cost are defined, they will be printed as well. Args: None Returns: None """ nodes, con, matl = self.cleaned_params() s = '\n' if self.deflection is not None: s += ' Nodes Deflections \n' s += ' # x y z dx dy dz \n' for i, line in enumerate(np.concatenate((nodes, self.deflection[:, :3, 0]), axis=1)): s += ' {:>3d} {: .2f} {: .2f} {: .2f} {: .3e} {: .3e} {: .3e} \n'.format( i, line[0], line[1], line[2], line[3], line[4], line[5]) else: s += ' Nodes Deflections \n' s += ' # x y z dx dy dz \n' for i, line in enumerate(nodes): s += ' {:>3d} {: .2f} {: .2f} {: .2f} Undefined \n'.format( i, line[0], line[1], line[2]) s += '\n' if self.fos is not None: s += ' Edges \n' s += ' Start End Property \n' s += ' # Node Node Type FoS \n' for i, line in enumerate(np.concatenate((con, matl.reshape(matl.shape[0], 1), self.fos), axis=1)): s += ' {:>3d} {:>3d} {:>3d} {:>3d} {:>7.2f} \n'.format( i, line[0].astype(int), line[1].astype(int), line[2].astype(int), line[3]) else: s += ' Edges \n' s += ' Start End Property \n' s += ' # Node Node Type FoS \n' for i, line in enumerate(np.concatenate((con, matl.reshape(matl.shape[0], 1)), axis=1)): s += ' {:>3d} {:>3d} {:>3d} {:>3d} Undefined \n'.format( i, line[0].astype(int), line[1].astype(int), line[2].astype(int)) s += '\n' if self.mass is not None: s += 'Mass: {:.3f} kg \n'.format(self.mass) else: s += 'Mass: Undefined \n' if self.cost is not None: s += 'Cost: $ {:.2f} \n '.format(self.cost) else: s += 'Cost: Undefined \n' return s
[docs] def plot(self, domain=None, loads=None, fixtures=None, deflection=False, load_scale=None, def_scale=100, ax=None, fig=None, setup_only=False): """Plots a truss object as a 3D wireframe Args: self (Truss object): truss to be plotted. Must have user_spec_nodes, rand_nodes, edges defined. domain (ndarray): (optional) axis limits in x,y,z, specified as a 3x2 array: [[xmin, xmax],[ymin,ymax],[zmin,zmax]]. loads (ndarray): (optional) Array of loads to be plotted as arrows. Specified as nx6 array, each row corresponding to the load at the node matching the row #. Load format: [Fx,Fy,Fz,Mx,My,Mz] fixtures (ndarray): (optional) Array of fixtures to be plotted as blobs. Specified as an nx6 array, each row corresponding to fixtures at the node matching the row #. Format: [Dx,Dy,Dz,Rx,Ry,Rz] value of 1 means fixed in that direction, value of zero is free. deflection (bool): If True, deflections will be plotted superposed on the undeformed structure. Relative size of deflections is governed by *def_scale*. load_scale (float): Size load vector arrows should be scaled by. def_scale (float): Scaling for deflections. *def_scale*=1 means actual size, larger than 1 magnifies. ax (axis): Axis to plot truss on, if an axis is passed to the function, the function is being called by ProgMon and *prog* is set to 1. If axis is none, a new one is created. fig (fig): Figure belonging to the axis. setup_only (boolean): If true, only the loads and fixtures are plotted. Returns: None """ nodes, con, matl = self.cleaned_params() num_con = con.shape[0] size_scale = (nodes.max(axis=0)-nodes.min(axis=0)).max() edge_vec_start = nodes[con[:, 0], :] # sfr edge_vec_end = nodes[con[:, 1], :] # sfr if load_scale is None and loads is not None: load_scale = size_scale/np.abs(loads).max()/5 if ax is None: fig = plt.figure() ax = fig.gca(projection='3d') prog = 0 ax.set_title('Truss') else: prog = 1 # currently in progress monitor ax.set_title('Truss Evolution') ax.set_xlabel('X [m]', fontsize=14, labelpad=10) ax.set_ylabel('Y [m]', fontsize=14, labelpad=10) ax.set_zlabel('Z [m]', fontsize=14, labelpad=10) ax.tick_params(labelsize='small') ax.view_init(30, -45) if domain is not None: ax.set_xlim(domain[:, 0]) ax.set_ylim(domain[:, 1]) ax.set_zlim(domain[:, 2]) if not setup_only: for i in range(num_con): ax.plot([edge_vec_start[i, 0], edge_vec_end[i, 0]], [edge_vec_start[i, 1], edge_vec_end[i, 1]], [edge_vec_start[i, 2], edge_vec_end[i, 2]], 'k-') if deflection and not setup_only: def_nodes = nodes + def_scale*self.deflection[:, :3, 0] def_edge_vec_start = def_nodes[con[:, 0], :] def_edge_vec_end = def_nodes[con[:, 1], :] for i in range(num_con): ax.plot([def_edge_vec_start[i, 0], def_edge_vec_end[i, 0]], [def_edge_vec_start[i, 1], def_edge_vec_end[i, 1]], [def_edge_vec_start[i, 2], def_edge_vec_end[i, 2]], 'b-', alpha=0.5) if loads is not None: ax.quiver(nodes[:, 0], nodes[:, 1], nodes[:, 2], loads[:, 0, 0], loads[:, 1, 0], loads[:, 2, 0], length=load_scale, pivot='tip', color='r') load_nodes = nodes[loads[:, :, 0].any(axis=1)] ax.scatter(load_nodes[:, 0], load_nodes[:, 1], load_nodes[:, 2], color='red', marker='o', depthshade=False, s=100) if fixtures is not None: fix_nodes = nodes[fixtures[:, :, 0].any(axis=1)] ax.scatter(fix_nodes[:, 0], fix_nodes[:, 1], fix_nodes[:, 2], color='green', marker='o', depthshade=False, s=100) if prog == 0: # only shows it if not being called within ProgMon plt.show() plt.gcf().savefig('final_result.png')