class PI(np.ndarray): """ A class representing a point of intersection (PI) of an alignment. Parameters ---------- x, y, z : float The x, y, and z coordinates. radius : float The radius of the horizontal curve. Use zero if a curve does not exist. """ # Custom properties x = propy.index_property(0) y = propy.index_property(1) z = propy.index_property(2) def __new__(cls, x, y, z=0, radius=0): obj = np.array([x, y, z], dtype='float').view(cls) obj.radius = radius return obj def __array_finalize__(self, obj): if obj is None: return self.radius = getattr(obj, 'radius', 0) __repr__ = propy.repr_method('x', 'y', 'z', 'radius')
class LoadCase(object): """ A class representing a structural load case. Parameters ---------- name : str The name of the load case. node_loads : list A list of :class:`.NodeLoad` to apply with the load case. elem_loads : list A list of :class:`.ElementLoad` to apply with the load case. """ # Custom properties name = propy.str_property('name') def __init__(self, name, node_loads=[], elem_loads=[]): self.name = name self.node_loads = node_loads self.elem_loads = elem_loads __repr__ = propy.repr_method('name', 'node_loads', 'elem_loads') def set_nodes(self, ndict): """ Sets the node references for all node loads assigned to the load case. Parameters ---------- ndict : dict A dictionary mapping node names to node objects. """ for n in self.node_loads: n.set_node(ndict) def set_elements(self, edict): """ Sets the element references for all element loads assigned to the load case. Parameters ---------- edict : dict A dictionary mapping element names to element objects. """ for e in self.elem_loads: e.set_element(edict)
class Material(object): """ A class representing an engineered material. Parameters ---------- name : str The name of the material. elasticity : float The modulus of elasticity. rigidity : float The modulus of rigidity. """ # Custom properties name = propy.str_property('name') def __init__(self, name, elasticity, rigidity=0): self.name = name self.elasticity = elasticity self.rigidity = rigidity __repr__ = propy.repr_method('name', 'elasticity', 'rigidity')
class ElementGroup(object): """ A class representing a group of element properties. Parameters ---------- name : str The name of the group. section : :class:`.CrossSection` The group cross section. material : :class:`.Material` The group material. """ # Custom properties name = propy.str_property('name') def __init__(self, name, section, material): self.name = name self.section = section self.material = material __repr__ = propy.repr_method('name', 'section', 'material')
class SurveyPoint(np.ndarray): """ A class representing a survey point. Parameters ---------- x, y, z : float The x, y, and z coordinates. """ # Custom properties x = propy.index_property(0) y = propy.index_property(1) z = propy.index_property(2) def __new__(cls, x, y, z, **kwargs): obj = np.array([x, y, z], dtype='float').view(cls) obj.meta = dict(**kwargs) return obj def __array_finalize__(self, obj): if obj is None: return self.meta = getattr(obj, 'meta', {}) __repr__ = propy.repr_method('x', 'y', 'z', 'meta')
class Alignment(object): """ A class representing a survey alignment. Parameters ---------- name : str Name of alignment. pis : list A list of :class:`.PI`. stakes : list A list of :class:`.SurveyStake`. grid : float The grid size used for spatial hash generation. view_offset : float The offset beyond which points will be ignored when generating station coordinates from global coordinates. view_margin : float The station margin at the beginning and end of the alignment. Beyond this threshold, generated station coordinates from global coordinates will be ignored. Examples -------- .. plot:: ../examples/survey/alignment_ex1.py :include-source: """ BISC_TOL = 1e-4 # Bisector station tolerance # Custom properties name = propy.str_property('name') def __init__(self, name, pis=[], stakes=[], grid=10, view_offset=15, view_margin=15): self.name = name self.pis = pis self.stakes = stakes self.grid = grid self.view_offset = view_offset self.view_margin = view_margin __repr__ = propy.repr_method('name', 'grid', 'view_offset', 'view_margin', 'pis', 'stakes') def set_stake_xy(self): """ Sets the xy coordinates for all station stakes assigned to the alignment. """ obj = [] p = [] for x in self.stakes: if x._type == 'station': obj.append(x) p.append((x.station, x.offset, x.rotation)) p = np.array(p) c, s = np.cos(p[:, 2]), np.sin(p[:, 2]) c, s = np.column_stack([c, -s]), np.column_stack([s, c]) b = self.coordinates(p[:, 0]) p = self.coordinates(p[:, :2]) p -= b c = np.einsum('ij,ij->i', p, c) s = np.einsum('ij,ij->i', p, s) p = np.column_stack([c, s]) p += b for a, b in zip(obj, p): a[:2] = b def pi_coordinates(self): """ Returns an array of PI coordinates of shape (N, 3). """ if not self.pis: return np.zeros((0, 3), dtype='float') return np.array(self.pis, dtype='float') def pi_radii(self): """ Returns an array of PI horizontal curve radii of shape (N,). """ return np.array([x.radius for x in self.pis], dtype='float') def azimuths(self): """ Returns an array of alignment azimuths in the shape (N,). Each element of the array corresponds to a PI index and represents the azimuth of the alignment ahead of that PI. """ if not self.pis: return np.zeros(0, dtype='float') elif len(self.pis) == 1: return np.zeros(1, dtype='float') x = self.pi_coordinates() dx = x[1:, :2] - x[:-1, :2] az = np.arctan2(dx[:, 0], dx[:, 1]) az = np.append(az, az[-1]) return np.asarray(az, dtype='float') def deflection_angles(self): """ Returns an array of PI deflection angles in the shape (N,). The angle is negative for turns to the left and positive for turns to the right. """ if not self.pis: return np.zeros(0, dtype='float') elif len(self.pis) == 1: return np.zeros(1, dtype='float') az = self.azimuths() da = az[1:] - az[:-1] i = (np.abs(da) > np.pi) da[i] -= 2 * np.pi * np.sign(da[i]) da = np.insert(da, 0, 0) return np.asarray(da, dtype='float') def tangent_ordinates(self): """ Returns an array of tangent ordinates corresponding to each PI in the shape (N,). This value is the horizontal distance between the PI and PC and PI and PT. """ r = self.pi_radii() da = self.deflection_angles() return r * np.abs(np.tan(da / 2)) def curve_lengths(self): """ Returns an array of horizontal curve lengths corresponding to each PI in teh shape (N,). This value is the station distance between the PC and PT. """ r = self.pi_radii() da = self.deflection_angles() return r * np.abs(da) def middle_ordinates(self): """ Returns an array of middle ordinate distances corresponding to each PI in the shape (N,). This value is the horizontal distance between the MPC and midpoint of the chord line between the PC and PT. """ r = self.pi_radii() da = np.abs(self.deflection_angles()) return r * (1 - np.cos(da / 2)) def external_ordinates(self): """ Returns an array of external ordinates corresponding to each PI in the shape (N,). This is the horizontal distance between the MPC and PI. """ r = self.pi_radii() da = self.deflection_angles() return r * np.abs(np.tan(da / 2) * np.tan(da / 4)) def chord_distances(self): """ Returns an array of chord distances corresponding to each PI in teh shape (N,). This is the straight line horizontal distance between the PC and PT. """ r = self.pi_radii() da = np.abs(self.deflection_angles()) return 2 * r * np.sin(da / 2) def pt_coordinates(self): """ Returns an array of (x, y) coordinates for the Point of Tangents (PT) in the shape (N, 2). """ if not self.pis: return np.zeros((0, 3), dtype='float') pi = self.pi_coordinates() az = self.azimuths() t = self.tangent_ordinates() t = np.expand_dims(t, 1) uv = np.column_stack([np.sin(az), np.cos(az)]) pt = pi[:, :2] + t * uv return np.asarray(pt, dtype='float') def pc_coordinates(self): """ Returns an array of (x, y) coordinates for the Point of Curves (PC) in the shape (N, 2). """ if not self.pis: return np.zeros((0, 3), dtype='float') pi = self.pi_coordinates() az = self.azimuths() da = self.deflection_angles() t = self.tangent_ordinates() t = np.expand_dims(t, 1) az -= da uv = np.column_stack([np.sin(az), np.cos(az)]) pc = pi[:, :2] - t * uv return np.asarray(pc, dtype='float') def mpc_coordinates(self): """ Returns an array of (x, y) coordinates for the Midpoint of Curves (MPC) in the shape (N, 2). """ if not self.pis: return np.zeros((0, 3), dtype='float') pi = self.pi_coordinates() az = self.azimuths() da = self.deflection_angles() e = self.external_ordinates() az += (np.pi - da) / 2 da = np.expand_dims(da, 1) e = np.expand_dims(e, 1) uv = np.column_stack([np.sin(az), np.cos(az)]) mpc = pi[:, :2] + np.sign(da) * e * uv return np.asarray(mpc, dtype='float') def rp_coordinates(self): """ Returns an array of (x, y) coordinates for the Radius Points (RP) in the shape (N, 2). """ if not self.pis: return np.zeros((0, 3), dtype='float') pi = self.pi_coordinates() az = self.azimuths() da = self.deflection_angles() e = self.external_ordinates() e = np.expand_dims(e, 1) r = self.pi_radii() r = np.expand_dims(r, 1) az += (np.pi - da) / 2 uv = np.column_stack([np.sin(az), np.cos(az)]) da = np.expand_dims(da, 1) rp = pi[:, :2] + np.sign(da) * (e + r) * uv return np.asarray(rp, dtype='float') def pt_stations(self): """ Returns an array of (x, y) coordinates for the Point of Tangents (PT) in the shape (N, 2). """ if not self.pis: return np.zeros(0, dtype='float') x = self.pi_coordinates() tan = self.tangent_ordinates() dist = np.linalg.norm(x[:-1, :2] - x[1:, :2], axis=1) dist = np.insert(dist, 0, 0) dist += self.curve_lengths() - tan sta = np.cumsum(dist) sta[1:] -= np.cumsum(tan[:-1]) return np.asarray(sta, dtype='float') def pc_stations(self): """ Returns an array of stations for the Point of Curves (PC) in the shape (N,). """ if not self.pis: return np.zeros(0, dtype='float') sta = self.pt_stations() - self.curve_lengths() return np.asarray(sta, dtype='float') def mpc_stations(self): """ Returns an array of stations for the Midpoint of Curves (MPC) in the shape (N,). """ return 0.5 * (self.pt_stations() + self.pc_stations()) def poc_transforms(self): """ Returns the POC transforms in the shape (N, 2, 2). These transforms project (x, y) global coordinates to (offset, station) station coordinates relative to the PI angle bisector. """ az = self.azimuths() da = self.deflection_angles() l = az - da / 2 t = l + np.pi / 2 t = np.column_stack([np.sin(t), np.cos(t), np.sin(l), np.cos(l)]) return t.reshape(t.shape[0], 2, 2) def pot_transforms(self): """ Returns the POT transforms in the shape (N, 2, 2). These transforms project (x, y) global coordinates to (offset, station) station coordinates relative to the tangent line between PI's. """ l = self.azimuths() t = l + np.pi / 2 t = np.column_stack([np.sin(t), np.cos(t), np.sin(l), np.cos(l)]) return t.reshape(t.shape[0], 2, 2) def segment_indices(self, stations): """ Determines the segment type and PI indices corresponding to the specified stations. Returns an array of shape (N, 2). The first column of the array contains 1 if the station is located along an alignment tangent or 2 if the station is located on a horizontal curve or alignment bisector. The second column contains the index corresponding to the PI where the point is located. Parameters ---------- stations : array An array of stations of shape (N,). """ sta = np.asarray(stations) pc_sta = self.pc_stations() pt_sta = self.pt_stations() s = SpatialHash(np.expand_dims(sta, 1), self.grid) # Set values beyond alignment limits r = np.zeros((sta.shape[0], 2), dtype='int') r[sta < 0] = 1, 0 r[sta > pt_sta[-1]] = 1, pt_sta.shape[0] - 1 # POT segments ah = np.expand_dims(pc_sta[1:], 1) bk = np.expand_dims(pt_sta[:-1], 1) for i, (a, b) in enumerate(zip(ah, bk)): f = s.query_range(b, a, 0) r[f] = 1, i # POC segments f = (self.curve_lengths() == 0) pc_sta[f] -= Alignment.BISC_TOL pt_sta[f] += Alignment.BISC_TOL ah = np.expand_dims(pt_sta[1:-1], 1) bk = np.expand_dims(pc_sta[1:-1], 1) for i, (a, b) in enumerate(zip(ah, bk)): f = s.query_range(b, a, 0) r[f] = 2, i + 1 return r def _pot_coordinates(self, result, seg, sta_coords): """ Assigns the POT coordinates for :meth:`.coordinates`. Parameters ---------- result : array The array to which the results will be added. seg : array The segment indices array. sta_coords : array An array of station coordinates of shape (N, 2). """ f = (seg[:, 0] == 1) if not f.any(): return sta = np.expand_dims(sta_coords[f, 0], 1) off = np.expand_dims(sta_coords[f, 1], 1) i = seg[f, 1] t = self.pot_transforms()[i] tx, ty = t[:, 0], t[:, 1] pt_coord = self.pt_coordinates()[i] pt_sta = np.expand_dims(self.pt_stations()[i], 1) result[f] = tx * off + ty * (sta - pt_sta) + pt_coord def _poc_bisc_coordinates(self, result, seg, sta_coords): """ Assigns the POC bisector coordinates for :meth:`.coordinates`. Parameters ---------- result : array The array to which the results will be added. seg : array The segment indices array. sta_coords : array An array of station coordinates of shape (N, 2). """ f = (seg[:, 0] == 2) & (self.curve_lengths() == 0)[seg[:, 1]] if not f.any(): return off = np.expand_dims(sta_coords[f, 1], 1) i = seg[f, 1] tx = self.poc_transforms()[i, 0] rp_coord = self.rp_coordinates()[i] result[f] = tx * off + rp_coord def _poc_curve_coordinates(self, result, seg, sta_coords): """ Assigns the POC curve coordinates for :meth:`.coordinates`. Parameters ---------- result : array The array to which the results will be added. seg : array The segment indices array. sta_coords : array An array of station coordinates of shape (N, 2). """ l = self.curve_lengths() f = (seg[:, 0] == 2) & (l != 0)[seg[:, 1]] if not f.any(): return sta = sta_coords[f, 0] off = sta_coords[f, 1] i = seg[f, 1] tx = self.poc_transforms()[i, 0] mpc_sta = self.mpc_stations()[i] rp_coord = self.rp_coordinates()[i] da = self.deflection_angles()[i] r = np.expand_dims(self.pi_radii()[i], 1) beta = da * (mpc_sta - sta) / l[i] c, s = np.cos(beta), np.sin(beta) c, s = np.column_stack([c, -s]), np.column_stack([s, c]) c = np.einsum('ij,ij->i', tx, c) s = np.einsum('ij,ij->i', tx, s) tx = np.column_stack([c, s]) da = np.sign(np.expand_dims(da, 1)) off = np.expand_dims(off, 1) result[f] = tx * (off - da * r) + rp_coord def coordinates(self, sta_coords): """ Returns the (x, y) or (x, y, z) global coordinates corresponding to the input station coordinates. Result is in the shape of (N, 2) of (N, 3). Parameters ---------- sta_coords : array An array of (station), (station, offset), or (station, offset, z) coordinates of the shape (N,), (N, 2) or (N, 3). """ sta_coords = np.asarray(sta_coords) # If shape is (N,), add zero offsets if len(sta_coords.shape) == 1: sta_coords = np.column_stack( [sta_coords, np.zeros(sta_coords.shape[0])]) result = np.zeros((sta_coords.shape[0], 2), dtype='float') seg = self.segment_indices(sta_coords[:, 0]) self._pot_coordinates(result, seg, sta_coords) self._poc_bisc_coordinates(result, seg, sta_coords) self._poc_curve_coordinates(result, seg, sta_coords) # Add z coordinate to result if available if sta_coords.shape[1] == 3: result = np.column_stack([result, sta_coords[:, 2]]) return np.asarray(result, dtype='float') def _pot_station_coordinates(self, result, spatial_hash, coords): """ Adds the POT station coordinates within the view. Parameters ---------- result : dict The dictionary to which the results will be added. spatial_hash : array The spatial hash. coords : array An array of coordinates of shape (N, 2) or (N, 3). """ t = self.pot_transforms() pt_sta = self.pt_stations() pt_coord = self.pt_coordinates() bk = self.pt_coordinates()[:-1] ah = self.pc_coordinates()[1:] if t.shape[0] > 0: bk[0] -= self.view_margin * t[0, 1] ah[-1] += self.view_margin * t[-1, 1] for i, (a, b) in enumerate(zip(ah, bk)): f = spatial_hash.query_range(b, a, self.view_offset) if f.shape[0] == 0: continue delta = coords[f, :2] - pt_coord[i] sta = np.dot(delta, t[i, 1]) + pt_sta[i] off = np.dot(delta, t[i, 0]) if coords.shape[1] == 3: p = np.column_stack([sta, off, coords[f, 2]]) else: p = np.column_stack([sta, off]) for n, m in enumerate(f): if m not in result: result[m] = [] result[m].append(p[n]) def _poc_station_coordinates(self, result, spatial_hash, coords): """ Adds the POC station coordinates within the view. Parameters ---------- result : dict The dictionary to which the results will be added. spatial_hash : array The spatial hash. coords : array An array of coordinates of shape (N, 2) or (N, 3). """ l = self.curve_lengths() t = self.poc_transforms() da = self.deflection_angles() pc_sta = self.pc_stations() pt_sta = self.pt_stations() rp_coord = self.rp_coordinates() pt_coord = self.pt_coordinates() for i in range(1, len(self.pis) - 1): r = self.pis[i].radius ro = r + self.view_offset ri = max(r - self.view_offset, 0) f = spatial_hash.query_point(rp_coord[i], ro, ri) if f.shape[0] == 0: continue if l[i] == 0: # Angle bisector delta = coords[f, :2] - pt_coord[i] sta = np.dot(delta, t[i, 1]) + pt_sta[i] off = np.dot(delta, t[i, 0]) g = ((np.abs(off) <= self.view_offset) & (sta >= pt_sta[i] - Alignment.BISC_TOL) & (sta <= pt_sta[i] + Alignment.BISC_TOL)) else: # Horizontal curve delta = pt_coord[i] - rp_coord[i] delta = np.arctan2(delta[0], delta[1]) p = coords[f, :2] - rp_coord[i] delta -= np.arctan2(p[:, 0], p[:, 1]) sta = pt_sta[i] - (l[i] / da[i]) * delta off = np.sign(da[i]) * (r - np.linalg.norm(p, axis=1)) g = (sta >= pc_sta[i]) & (sta <= pt_sta[i]) if coords.shape[1] == 3: p = np.column_stack([sta, off, coords[f, 2]])[g] else: p = np.column_stack([sta, off])[g] for n, m in enumerate(f[g]): if m not in result: result[m] = [] result[m].append(p[n]) def station_coordinates(self, coordinates): """ Finds the (station, offset) or (station, offset, z) coordinates for the input global coordinates. Returns a dictionary of point indices with arrays of shape (N, 2) or (N, 3). If a point index is not in the dictionary, then no points are located along the alignment within the view threshold. Parameters ---------- coordinates : array An array of (x, y) or (x, y, z) global coordinates in the shape (N, 2) or (N, 3). """ coordinates = np.asarray(coordinates) s = SpatialHash(coordinates[:, :2], self.grid) result = {} self._pot_station_coordinates(result, s, coordinates) self._poc_station_coordinates(result, s, coordinates) for k, x in result.items(): result[k] = np.array(x, dtype='float') return result def plot_plan(self, ax=None, step=1, symbols={}): """ Plots a the plan view for the alignment. Parameters ---------- ax : :class:`matplotlib.axes.Axes` The axex to which to add the plot. If None, a new figure and axes will be created. step : float The step interval to use for plotting points along horizontal curves. symbols : dict A dictionary of symbols to use for the plot. The following keys are used: * `pi`: PI point symbol, default is 'r.' * `rp`: RP point symbol, default is 'c.' * `pc`: PC point symbol, default is 'b.' * `pt`: PT point symbol, default is 'b.' * `alignment`: Alignment lines, default is 'b-' * `stakes`: Stake symbols, default is 'rx' Examples -------- .. plot:: ../examples/survey/alignment_ex1.py :include-source: """ if ax is None: x = self.pi_coordinates()[:, :2] mx = x.max(axis=0) c = 0.5 * (mx + x.min(axis=0)) r = 1.1 * (np.max(mx - c) + self.view_offset + self.view_margin) xlim, ylim = np.column_stack([c - r, c + r]) fig = plt.figure() ax = fig.add_subplot(111, title=self.name, xlim=xlim, ylim=ylim, xlabel='X', ylabel='Y', aspect='equal') ax.grid('major', alpha=0.2) sym = dict(pi='r.', rp='c.', pc='b.', pt='b.', alignment='b-', stakes='rx') sym.update(symbols) pt = self.pt_coordinates() pc = self.pc_coordinates() if sym['alignment'] is not None: for a, b in zip(pt[:-1], pc[1:]): x = np.array([a, b]) ax.plot(x[:, 0], x[:, 1], sym['alignment']) for a, b in zip(self.pt_stations(), self.pc_stations()): if a != b: n = int(np.ceil((a - b) / step)) sta = np.linspace(b, a, n) x = self.coordinates(sta) ax.plot(x[:, 0], x[:, 1], sym['alignment']) if sym['pi'] is not None: x = self.pi_coordinates() ax.plot(x[:, 0], x[:, 1], sym['pi']) if sym['rp'] is not None: x = self.rp_coordinates() ax.plot(x[:, 0], x[:, 1], sym['rp']) if sym['pt'] is not None: ax.plot(pt[:, 0], pt[:, 1], sym['pt']) if sym['pc'] is not None: ax.plot(pc[:, 0], pc[:, 1], sym['pc']) if sym['stakes'] is not None and len(self.stakes) > 0: self.set_stake_xy() x = np.array(self.stakes) ax.plot(x[:, 0], x[:, 1], sym['stakes']) return ax
class TIN(object): """ A class for creating triangulated irregular networks (TIN) models for 3D surfaces. Includes methods for performing elevation and distance queries. Parameters ---------- name : str The name of the model. points : array An array of points of shape (N, 3). breaklines : list A list of arrays of points representing breakline polylines. max_edge : float The maximum edge length beyond which simplices will not be included in the triangulation. If None, no simplices will be removed. step : float The setup interval for generated points along breaklines. grid : float The grid spacing used for the created spatial hash. Examples -------- The following example creates a TIN model in the shape of a pyramid then performs distances queries to its surface: .. plot:: ../examples/survey/tin_ex1.py :include-source: """ def __init__(self, name, points=[], breaklines=[], max_edge=None, step=0.1, grid=10): self.name = name self.breaklines = breaklines self.max_edge = max_edge self.step = step self._create_triangulation(points, grid) __repr__ = propy.repr_method('name') def _create_triangulation(self, points, grid): """ Creates the Delaunay trianguation and spatial hash. Parameters ---------- points : array An array of points of shape (N, 3). grid : float The spatial hash grid spacing. """ points = np.asarray(points) b = self.breakpoints() if b.shape[0] > 0: if points.shape[0] > 0: points = points[points[:, 2].argsort()[::-1]] points = np.concatenate([b, points]) else: points = b self.points = points self.tri = Delaunay(points[:, :2]) self.hash = SpatialHash(points[:, :2], grid) self._remove_simplices() def _remove_simplices(self): """ Removes all simplices with any edge greater than the max edge. """ if self.max_edge is not None: p = self.tri.points s = self.tri.simplices a, b, c = p[s[:, 0]], p[s[:, 1]], p[s[:, 2]] f = ((np.linalg.norm(a - b) <= self.max_edge) & (np.linalg.norm(b - c) <= self.max_edge) & (np.linalg.norm(a - c) <= self.max_edge)) self.tri.simplices = s[f] def breakpoints(self): """ Returns an array of breakpoints for the assigned breaklines. The breakpoints are sorted by z coordinate from greatest to least. """ points = [np.zeros((0, 3), dtype='float')] for line in self.breaklines: line = np.asarray(line) for i, (a, b) in enumerate(zip(line[:-1], line[1:])): m = a - b n = int(np.ceil(np.linalg.norm(m) / self.step)) if n < 2: n = 2 if i == 0 else 1 x = np.expand_dims(np.linspace(0, 1, n), 1) y = m * x + b points.append(y) points = np.concatenate(points) points = points[points[:, 2].argsort()[::-1]] return points def find_simplices(self, points): """ Finds the simplices which contain the (x, y) point. Returns the simplex index if a single point is input or an array of simplex indices if multiple points are input. If the returned simplex index is -1, then the (x, y) point is not contained within any simplices. Parameters ---------- points : array An array of points of shape (2,), (3,), (N, 2) or (N, 3). """ points = np.asarray(points) if len(points.shape) == 1: points = points[:2] else: points = points[:, :2] return self.tri.find_simplex(points) def _simplex_indices(self, indexes): """ Returns the simplex indices that include the input point indices. Parameters ---------- indexes : list A list of point indices for which connected simplices will be returned. """ indexes = set(indexes) a = [] for i, x in enumerate(self.tri.simplices): for j in x: if j in indexes: a.append(i) a = np.unique(a).astype('int') return a def query_simplices(self, point, radius): """ Returns the indices of all simplices that have a corner within the specified radius of the input point. Parameters ---------- point : array A point of shape (2,) or (3,). radius : float The xy-plane radius used for the query. """ point = np.asarray(point) i = self.hash.query_point(point[:2], radius) return self._simplex_indices(i) def normal(self, simplex): """ Returns the normal vector for the specified simplex. The returned normal vector is also a unit vector. Parameters ---------- simplex : int The index of the simplex. """ p = self.points s = self.tri.simplices[simplex] a, b, c = p[s[0]], p[s[1]], p[s[2]] n = np.cross(b - a, c - a) return n / np.linalg.norm(n) def elevation(self, point): """ Returns the elevation of the TIN surface at the input point. Returns NaN if the TIN surface does not exist at that point. Parameters ---------- point : array A point for which the elevation will be calculated. The point must be of shape (2,) or (3,). """ point = np.asarray(point) s = self.find_simplices(point) if s == -1: return np.nan p = self.points n = self.normal(s) s = self.tri.simplices[s] if n[2] != 0: # Plane elevation a = p[s[0]] a = np.dot(n, a) b = np.dot(n[:2], point[:2]) return (a - b) / n[2] # Vertical plane. Use max edge elevation zs = [] a, b, c = p[s[0]], p[s[1]], p[s[2]] for v, w in ((a, b), (a, c), (b, c)): dv = np.linalg.norm(point[:2] - v[:2]) dvw = np.linalg.norm(w[:2] - v[:2]) z = (w[2] - v[2]) * dv / dvw + v[2] zs.append(z) return max(zs) def barycentric_coords(self, point, simplex): """ Returns the local barycentric coordinates for the input point. If any of the coordinates are less than 0, then the projection of the point onto the plane of the triangle is outside of the triangle. Parameters ---------- point : array A point for which barycentric coordinates will be calculated. The point must be of shape (3,). simplex : int The index of the simplex. """ point = np.asarray(point) p = self.points n = self.normal(simplex) s = self.tri.simplices[simplex] a, b, c = p[s[0]], p[s[1]], p[s[2]] u = b - a u = u / np.linalg.norm(u) v = np.cross(u, n) x1, y1 = np.dot(a, u), np.dot(a, v) x2, y2 = np.dot(b, u), np.dot(b, v) x3, y3 = np.dot(c, u), np.dot(c, v) det = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3) x1, y1, x2, y2 = y2 - y3, x3 - x2, y3 - y1, x1 - x3 x, y = np.dot(point, u), np.dot(point, v) l1 = (x1 * (x - x3) + y1 * (y - y3)) / det l2 = (x2 * (x - x3) + y2 * (y - y3)) / det l3 = 1 - l1 - l2 return np.array([l1, l2, l3]) def query_distances(self, point, radius): """ Finds the closest distances to all simplices within the specified xy-plane radius. Parameters ---------- point : array An array of shape (3,). radius : float The radius within the xy-plane in which simplices will be queried. Returns ------- distances : array An array of distances to simplices of shape (N,). tin_points : array An array of closest simplex points of shape (N, 3). """ point = np.asarray(point) simplices = self.query_simplices(point, radius) p = self.points s = self.tri.simplices[simplices] bary = [self.barycentric_coords(point, x) for x in simplices] bary = np.min(bary, axis=1) tin = np.zeros((simplices.shape[0], 3), dtype='float') dist = np.zeros(simplices.shape[0], dtype='float') for i, x in enumerate(bary): if x >= 0: # Plane distance n = self.normal(simplices[i]) d = np.dot(p[s[i, 0]] - point, n) tin[i] = d * n + point dist[i] = abs(d) continue # Edge distance a, b, c = p[s[i]] dist[i] = float('inf') for v, w in ((a, b), (b, c), (a, c)): u = w - v m = np.linalg.norm(u) u = u / m proj = np.dot(point - v, u) if proj <= 0: r = v elif proj >= m: r = w else: r = proj * u + v d = np.linalg.norm(r - point) if d < dist[i]: tin[i], dist[i] = r, d f = dist.argsort() return dist[f], tin[f] def plot_surface_3d(self, ax=None, cmap='terrain'): """ Plots a the rendered TIN surface in 3D Parameters ---------- ax : :class:`matplotlib.axes.Axes` The axes to which the plot will be added. If None, a new figure and axes will be created. cmap : str The name of the color map to use. Examples -------- .. plot:: ../examples/survey/tin_ex1.py :include-source: """ if ax is None: mx = self.points.max(axis=0) c = 0.5 * (mx + self.points.min(axis=0)) r = 1.1 * np.max(mx - c) xlim, ylim, zlim = np.column_stack([c - r, c + r]) fig = plt.figure() ax = fig.add_subplot(111, title=self.name, projection='3d', xlim=xlim, ylim=ylim, zlim=zlim, aspect='equal') x = self.points ax.plot_trisurf(x[:, 0], x[:, 1], x[:, 2], triangles=self.tri.simplices, cmap=cmap) return ax def plot_surface_2d(self, ax=None): """ Plots a the triangulation in 2D. Parameters ---------- ax : :class:`matplotlib.axes.Axes` The axes to which the plot will be added. If None, a new figure and axes will be created. Examples -------- .. plot:: ../examples/survey/tin_ex3.py :include-source: """ if ax is None: mx = self.points[:, :2].max(axis=0) c = 0.5 * (mx + self.points[:, :2].min(axis=0)) r = 1.1 * np.max(mx - c) xlim, ylim = np.column_stack([c - r, c + r]) fig = plt.figure() ax = fig.add_subplot(111, title=self.name, xlim=xlim, ylim=ylim, aspect='equal') x = self.points ax.triplot(x[:, 0], x[:, 1], triangles=self.tri.simplices) return ax def plot_contour_2d(self, ax=None, cmap='terrain'): """ Plots a the rendered TIN surface in 3D Parameters ---------- ax : :class:`matplotlib.axes.Axes` The axes to which the plot will be added. If None, a new figure and axes will be created. cmap : str The name of the color map to use. Examples -------- .. plot:: ../examples/survey/tin_ex2.py :include-source: """ if ax is None: mx = self.points[:, :2].max(axis=0) c = 0.5 * (mx + self.points[:, :2].min(axis=0)) r = 1.1 * np.max(mx - c) xlim, ylim = np.column_stack([c - r, c + r]) fig = plt.figure() ax = fig.add_subplot(111, title=self.name, xlim=xlim, ylim=ylim, aspect='equal') x = self.points contourf = ax.tricontourf(x[:, 0], x[:, 1], x[:, 2], cmap=cmap) contour = ax.tricontour(x[:, 0], x[:, 1], x[:, 2], colors='black') ax.clabel(contour, inline=True, fontsize=6) fig.colorbar(contourf) return ax
class Element(object): """ A class representing a structural element. Parameters ---------- name : str A unique name for the element. inode, jnode : str The names of the nodes at the i and j ends of the element. group : :class:`.ElementGroup` The group assigned to the element. symmetry : {None, 'x', 'y', 'xy'} The symmetry of the element. roll : float The counter clockwise angle of roll about the length axis. imx_free, imy_free, imz_free : bool The rotational fixities at the i-node about the local x, y, and z axes. jmx_free, jmy_free, jmz_free : bool The rotational fixities at the j-node about the local x, y, and z axes. """ SYMMETRIES = (None, 'x', 'y', 'xy') TRANSFORMS = { 'x': {'p': 'x', 'x': 'p', 'y': 'xy', 'xy': 'y'}, 'y': {'p': 'y', 'x': 'xy', 'y': 'p', 'xy': 'x'}, } # Custom properties name = propy.str_property('name') inode = propy.str_property('inode_name') jnode = propy.str_property('jnode_name') symmetry = propy.enum_property('symmetry', set(SYMMETRIES)) imx_free = propy.bool_property('imx_free') imy_free = propy.bool_property('imy_free') imz_free = propy.bool_property('imz_free') jmx_free = propy.bool_property('jmx_free') jmy_free = propy.bool_property('jmy_free') jmz_free = propy.bool_property('jmz_free') inode_ref = propy.weakref_property('inode_ref') jnode_ref = propy.weakref_property('jnode_ref') def __init__(self, name, inode, jnode, group, symmetry=None, roll=0, unstr_length=None, imx_free=False, imy_free=False, imz_free=False, jmx_free=False, jmy_free=False, jmz_free=False): self.name = name self.inode = inode self.jnode = jnode self.group = group self.symmetry = symmetry self.roll = roll self.unstr_length = unstr_length self.imx_free = imx_free self.imy_free = imy_free self.imz_free = imz_free self.jmx_free = jmx_free self.jmy_free = jmy_free self.jmz_free = jmz_free self.inode_ref = None self.jnode_ref = None __repr__ = propy.repr_method( 'name', 'inode', 'jnode', 'group', 'symmetry', 'roll', 'imx_free', 'imy_free', 'imz_free', 'jmx_free', 'jmy_free', 'jmz_free' ) def __str__(self): return self.name def copy(self): """Returns a copy of the element.""" return copy.copy(self) def i_free(self): """Sets the i end rotational fixities to free. Returns the element.""" self.imx_free = self.imy_free = self.imz_free = True return self def j_free(self): """Sets the j end rotational fixities to free. Returns the element.""" self.jmx_free = self.jmy_free = self.jmz_free = True return self def free(self): """Sets the end rotational fixities to free. Returns the element.""" return self.i_free().j_free() def mx_free(self): """Sets the x rotational fixities to free. Returns the element.""" self.imx_free = self.jmx_free = True return self def my_free(self): """Sets the y rotational fixities to free. Returns the element.""" self.imy_free = self.jmy_free = True return self def mz_free(self): """Sets the z rotational fixities to free. Returns the element.""" self.imz_free = self.jmz_free = True return self def set_nodes(self, ndict): """ Sets the node references for the element. Parameters ---------- ndict : dict A dictionary that maps node names to :class:`.Node` objects. """ self.inode_ref = ndict[self.inode] self.jnode_ref = ndict[self.jnode] def get_nodes(self): """Returns the i and j node objects.""" if self.inode_ref is None or self.jnode_ref is None: raise ValueError('Node references have not been set.') return self.inode_ref, self.jnode_ref def get_unstr_length(self): """ If the unstressed length of the element is None, returns the initial distance between the nodes. If the unstressed length is a string, converts the string to a float and adds it to the initial distance between the nodes. Otherwise, returns the assigned unstressed length. """ if self.unstr_length is None: return self.length() elif isinstance(self.unstr_length, str): return self.length() + float(self.unstr_length) return self.unstr_length def length(self, di=(0, 0, 0), dj=(0, 0, 0)): """ Returns the length of the element between nodes. Parameters ---------- di, dj : array The deflections at the i and j ends of the element. """ xi, xj = self.get_nodes() di, dj = np.asarray(di), np.asarray(dj) delta = (xj - xi) + (dj - di) return np.linalg.norm(delta) def sym_elements(self): """Returns the symmetric elements for the element.""" def trans(name, *sym): t = Element.TRANSFORMS n = name.split('_') for x in sym: n[-1] = t[x][n[-1]] return '_'.join(n) def primary(): e = self.copy() e.name = '{}_p'.format(self.name) return e def x_sym(): e = self.copy() e.name = '{}_x'.format(self.name) e.inode = trans(self.inode, 'x') e.jnode = trans(self.jnode, 'x') return e def y_sym(): e = self.copy() e.name = '{}_y'.format(self.name) e.inode = trans(self.inode, 'y') e.jnode = trans(self.jnode, 'y') return e def xy_sym(): e = self.copy() e.name = '{}_xy'.format(self.name) e.inode = trans(self.inode, 'x', 'y') e.jnode = trans(self.jnode, 'x', 'y') return e if self.symmetry is None: return primary(), elif self.symmetry == 'x': return primary(), x_sym() elif self.symmetry == 'y': return primary(), y_sym() elif self.symmetry == 'xy': return primary(), x_sym(), y_sym(), xy_sym() def rotation_matrix(self, di=(0, 0, 0), dj=(0, 0, 0)): """ Returns the rotation matrix for the element. Parameters ---------- di, dj : array The deflections at the i and j ends of the element. """ xi, xj = self.get_nodes() di, dj = np.asarray(di), np.asarray(dj) dx, dy, dz = (xj - xi) + (dj - di) return rotation_matrix(dx, dy, dz, self.roll) def transformation_matrix(self, di=(0, 0, 0), dj=(0, 0, 0)): """ Returns the transformation matrix for the element. Parameters ---------- di, dj : array The deflections at the i and j ends of the element. """ xi, xj = self.get_nodes() di, dj = np.asarray(di), np.asarray(dj) dx, dy, dz = (xj - xi) + (dj - di) return transformation_matrix(dx, dy, dz, self.roll) def local_stiffness(self, di=(0, 0, 0), dj=(0, 0, 0)): """ Returns the local stiffness for the element. Parameters ---------- di, dj : array The deflections at the i and j ends of the element. """ group = self.group sect = group.section mat = group.material return local_stiffness( l=self.length(di, dj), lu=self.get_unstr_length(), a=sect.area, ix=sect.inertia_x, iy=sect.inertia_y, j=sect.inertia_j, e=mat.elasticity, g=mat.rigidity, imx_free=self.imx_free, imy_free=self.imy_free, imz_free=self.imz_free, jmx_free=self.jmx_free, jmy_free=self.jmy_free, jmz_free=self.jmz_free ) def global_stiffness(self, di=(0, 0, 0), dj=(0, 0, 0)): """ Returns the global stiffness matrix for the element. Parameters ---------- di, dj : array The deflections at the i and j ends of the element. """ di, dj = np.asarray(di), np.asarray(dj) t = self.transformation_matrix(di, dj) k = self.local_stiffness(di, dj) return t.T.dot(k).dot(t)
class Node(np.ndarray): """ A class representing a structural node. Parameters ---------- name : str A unique name for the node. x, y, z : float The x, y, and z coordinates of the node. symmetry : {None, 'x', 'y', 'xy'} The symmetry of the node. fx_free, fy_free, fz_free : bool The force fixities of the node in the x, y, and z directions. mx_free, my_free, mz_free : bool The moment fixities of the node about the x, y, and z axes. """ SYMMETRIES = (None, 'x', 'y', 'xy') # Custom properties name = propy.str_property('name') x = propy.index_property(0) y = propy.index_property(1) z = propy.index_property(2) symmetry = propy.enum_property('symmetry', set(SYMMETRIES)) fx_free = propy.bool_property('fx_free') fy_free = propy.bool_property('fy_free') fz_free = propy.bool_property('fz_free') mx_free = propy.bool_property('mx_free') my_free = propy.bool_property('my_free') mz_free = propy.bool_property('mz_free') def __new__(cls, name, x=0, y=0, z=0, symmetry=None, fx_free=True, fy_free=True, fz_free=True, mx_free=True, my_free=True, mz_free=True): obj = np.array([x, y, z], dtype='float').view(cls) obj.name = name obj.symmetry = symmetry obj.fx_free = fx_free obj.fy_free = fy_free obj.fz_free = fz_free obj.mx_free = mx_free obj.my_free = my_free obj.mz_free = mz_free return obj def __array_finalize__(self, obj): if obj is None: return self.name = getattr(obj, 'name', '') self.symmetry = getattr(obj, 'symmetry', None) self.fx_free = getattr(obj, 'fx_free', True) self.fy_free = getattr(obj, 'fy_free', True) self.fz_free = getattr(obj, 'fz_free', True) self.mx_free = getattr(obj, 'mx_free', True) self.my_free = getattr(obj, 'my_free', True) self.mz_free = getattr(obj, 'mz_free', True) __repr__ = propy.repr_method('name', 'x', 'y', 'z', 'symmetry', 'fx_free', 'fy_free', 'fz_free', 'mx_free', 'my_free', 'mz_free') def __str__(self): return self.name def copy(self): """Returns a copy of the node.""" return copy.copy(self) def f_fixed(self): """Sets the node force reactions to fixed.""" self.fx_free = self.fy_free = self.fz_free = False return self def m_fixed(self): """Sets the node moment reactions to fixed.""" self.mx_free = self.my_free = self.mz_free = False return self def fixed(self): """Sets the node force and moment reactions to fixed.""" return self.f_fixed().m_fixed() def fixities(self): """Returns the force and moment fixities for the node.""" return [ self.fx_free, self.fy_free, self.fz_free, self.mx_free, self.my_free, self.mz_free ] def sym_nodes(self): """Returns the symmetric nodes for the node.""" def primary(): n = self.copy() n.name = '{}_p'.format(self.name) return n def x_sym(): n = self.copy() n.name = '{}_x'.format(self.name) n[1] *= -1 return n def y_sym(): n = self.copy() n.name = '{}_y'.format(self.name) n[0] *= -1 return n def xy_sym(): n = self.copy() n.name = '{}_xy'.format(self.name) n[:2] *= -1 return n if self.symmetry is None: return primary(), elif self.symmetry == 'x': return primary(), x_sym() elif self.symmetry == 'y': return primary(), y_sym() elif self.symmetry == 'xy': return primary(), x_sym(), y_sym(), xy_sym()
class SurveyStake(np.ndarray): """ A class representing a survey stake. This method should be initialized using the :meth:`SurveyStake.init_xy` or :meth:`SurveyStake.init_station` class methods. """ TYPES = ('xy', 'station') # Custom properties x = propy.index_property(0) y = propy.index_property(1) z = propy.index_property(2) lock_z = propy.bool_property('lock_z') _type = propy.enum_property('_type', TYPES) def __new__(cls, x, y, z, station, offset, height, rotation, lock_z, _type, _init=False, **kwargs): if not _init: raise ValueError( 'SurveyStake should be initialized using the ' '`SurveyStake.init_xy` or `SurveyStake.init_station` methods ' 'in lieu of the standard initializer.') obj = np.array([x, y, z], dtype='float').view(cls) obj.station = station obj.offset = offset obj.height = height obj.rotation = rotation obj.lock_z = lock_z obj._type = _type obj.meta = dict(**kwargs) return obj def __array_finalize__(self, obj): if obj is None: return self.station = getattr(obj, 'station', 0) self.offset = getattr(obj, 'offset', 0) self.height = getattr(obj, 'height', 0) self.rotation = getattr(obj, 'rotation', 0) self.lock_z = getattr(obj, 'lock_z', False) self._type = getattr(obj, '_type', 'xy') self.meta = getattr(obj, 'meta', {}) __repr__ = propy.repr_method('_type', 'x', 'y', 'z', 'station', 'offset', 'lock_z', 'meta') @classmethod def init_xy(cls, x, y, z=0, height=0, rotation=0, lock_z=False, **kwargs): """ Initializes a survey stake based on an (x, y) global coordinate. Parameters ---------- x, y, z : float The x, y, and z coordinates. height : float The height of the point above z. rotation : float The rotation of the point about its base point. lock_z : float If False, the alignment will be snapped to the TIN (if applicable) during certain updates. Otherwise, the z coordinate will remain fixed. """ return cls(x=x, y=y, z=z, station=0, offset=0, height=height, rotation=rotation, lock_z=lock_z, _type='xy', _init=True, **kwargs) @classmethod def init_station(cls, station, offset=0, z=0, height=0, rotation=0, lock_z=False, **kwargs): """ Initializes a survey stake based on a survey station and offset. Parameters ---------- station : float The alignment survey station. offset : float The offset from the alignment. z : float The z coordinate. height : float The height of the point above z. rotation : float The rotation of the point about its base point. lock_z : float If False, the alignment will be snapped to the TIN (if applicable) during certain updates. Otherwise, the z coordinate will remain fixed. """ return cls(x=0, y=0, z=z, height=height, rotation=rotation, station=station, offset=offset, lock_z=lock_z, _type='station', _init=True, **kwargs)