├── .coveragerc ├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── civpy ├── __init__.py ├── config │ ├── __init__.py │ └── _matplotlib.py ├── math │ ├── __init__.py │ ├── linalg.py │ ├── linalg_test.py │ ├── optimize.py │ └── optimize_test.py ├── structures │ ├── __init__.py │ ├── element.py │ ├── element_group.py │ ├── element_load.py │ ├── element_load_test.py │ ├── element_test.py │ ├── load_case.py │ ├── load_case_test.py │ ├── material.py │ ├── material_test.py │ ├── node.py │ ├── node_load.py │ ├── node_load_test.py │ ├── node_test.py │ ├── structure.py │ └── structure_test.py └── survey │ ├── __init__.py │ ├── alignment.py │ ├── alignment_test.py │ ├── pi.py │ ├── pi_test.py │ ├── spatial_hash.py │ ├── spatial_hash_test.py │ ├── survey_point.py │ ├── survey_point_test.py │ ├── survey_stake.py │ ├── survey_stake_test.py │ ├── tin.py │ └── tin_test.py ├── docs ├── Makefile ├── _static │ ├── alignment.png │ ├── spatial_hash.png │ ├── structure.png │ ├── summary.png │ ├── tin.png │ └── truss.png ├── _templates │ ├── autosummary │ │ └── class.rst │ └── searchbox.html ├── conf.py ├── index.rst ├── make.bat ├── math.rst ├── releases │ └── .holder ├── structures.rst └── survey.rst ├── examples ├── structures │ ├── structure_ex1.py │ ├── structure_ex2.py │ └── structure_ex3.py └── survey │ ├── alignment_ex1.py │ ├── spatial_hash_ex1.py │ ├── spatial_hash_ex2.py │ ├── tin_ex1.py │ ├── tin_ex2.py │ └── tin_ex3.py ├── pytest.ini ├── requirements.txt ├── setup.cfg └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = civpy 3 | omit = 4 | setup.py 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | docs/generated 10 | *.ipynb 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/scipy-sphinx-theme"] 2 | path = docs/scipy-sphinx-theme 3 | url = https://github.com/scipy/scipy-sphinx-theme 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.5' 4 | - '3.6' 5 | # command to install dependencies 6 | install: 7 | - pip install .[test] 8 | # command to run tests 9 | script: 10 | - pytest --cov=./ 11 | - codecov 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Matt Pewsey 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CivPy 2 | 3 | [![Build Status](https://travis-ci.com/mpewsey/civpy.svg?branch=master)](https://travis-ci.com/mpewsey/civpy) 4 | [![Documentation Status](https://readthedocs.org/projects/civpy/badge/?version=latest)](https://civpy.readthedocs.io/en/latest/?badge=latest) 5 | [![codecov](https://codecov.io/gh/mpewsey/civpy/branch/master/graph/badge.svg?token=zbJbsGGSoL)](https://codecov.io/gh/mpewsey/civpy) 6 | 7 | ## About 8 | 9 | This package provides civil engineering tools and algorithms for creating 10 | survey and structure models in Python. 11 | 12 | ## Installation 13 | 14 | The development version of this package may be installed via pip: 15 | 16 | ```none 17 | pip install git+https://github.com/mpewsey/civpy#egg=civpy 18 | ``` 19 | 20 | ## Features 21 | 22 | ### Survey Tools 23 | 24 | 25 | 26 | 30 | 35 | 36 | 37 | 41 | 43 | 44 |
27 | spatial_hash
28 | Find points in N-dimensional spaces using spatial hash distance queries. 29 |
31 | tin
32 | Create ground surface triangulations, find surface elevations, and 33 | perform point distance queries to the surface. 34 |
38 | alignment
39 | Model survey alignments with or without horizontal curves and perform coordinate calculations. 40 |
42 |
45 | 46 | ### Structure Modeling 47 | 48 | 49 | 50 | 54 | 55 | 56 | 60 | 64 | 65 |
51 | tin
52 | Perform analysis of 2D and 3D trusses and manipulate node, element, and reaction results using Pandas data frames. 53 |
57 | structure
58 | Model structures with symmetry. 59 |
61 | truss
62 | Plot structure geometry in 3D or in 2D cross sections. 63 |
66 | -------------------------------------------------------------------------------- /civpy/__init__.py: -------------------------------------------------------------------------------- 1 | from . import config 2 | -------------------------------------------------------------------------------- /civpy/config/__init__.py: -------------------------------------------------------------------------------- 1 | from . import _matplotlib 2 | -------------------------------------------------------------------------------- /civpy/config/_matplotlib.py: -------------------------------------------------------------------------------- 1 | # This module configures matplotlib 2 | 3 | import os 4 | import sys 5 | import matplotlib 6 | from mpl_toolkits.mplot3d import Axes3D 7 | 8 | __all__ = [] 9 | 10 | 11 | # if sys.version_info[0] < 3 and 'DISPLAY' not in os.environ: 12 | # matplotlib.use('Agg') 13 | -------------------------------------------------------------------------------- /civpy/math/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ======================== 3 | Math (:mod:`civpy.math`) 4 | ======================== 5 | 6 | Contains math functions. 7 | 8 | Linear Algebra 9 | ============== 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | projection_angles 14 | rotation_matrix2 15 | rotation_matrix3 16 | rotate2 17 | rotate3 18 | 19 | 20 | Optimization 21 | ============ 22 | .. autosummary:: 23 | :toctree: generated/ 24 | 25 | fsolve 26 | """ 27 | 28 | from .linalg import * 29 | from .optimize import * 30 | -------------------------------------------------------------------------------- /civpy/math/linalg.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import numpy as np 6 | from math import cos, sin 7 | 8 | __all__ = [ 9 | 'projection_angles', 10 | 'rotation_matrix2', 11 | 'rotation_matrix3', 12 | 'rotate2', 13 | 'rotate3', 14 | ] 15 | 16 | 17 | def projection_angles(name): 18 | """ 19 | Returns the rotation angles for the specified projection. 20 | 21 | Parameters 22 | ---------- 23 | name : {'xy', 'xz', 'yz', 'yx', 'zx', 'zy'} 24 | The name of the projection. 25 | """ 26 | if name == 'xy': 27 | return 0, 0, 0 28 | elif name == 'xz': 29 | return -np.pi/2, 0, 0 30 | elif name == 'yz': 31 | return -np.pi/2, 0, -np.pi/2 32 | elif name == 'yx': 33 | return 0, np.pi, np.pi/2 34 | elif name == 'zx': 35 | return np.pi/2, np.pi/2, 0 36 | elif name == 'zy': 37 | return np.pi, np.pi/2, np.pi 38 | else: 39 | raise ValueError('Invalid projection name: {!r}.'.format(name)) 40 | 41 | 42 | def rotation_matrix2(angle): 43 | """ 44 | Returns the 2D rotation matrix. 45 | 46 | Parameters 47 | ---------- 48 | angle : float 49 | The counter clockwise rotation angle in radians. 50 | """ 51 | c, s = cos(angle), sin(angle) 52 | return np.array([[c, -s], [s, c]]) 53 | 54 | 55 | def rotation_matrix3(angle_x=0, angle_y=0, angle_z=0): 56 | """ 57 | Returns the 3D rotation matrix. 58 | 59 | Parameters 60 | ---------- 61 | angle : float 62 | """ 63 | if angle_x != 0: 64 | c, s = cos(angle_x), sin(angle_x) 65 | r = np.array([[1, 0, 0], [0, c, -s], [0, s, c]]) 66 | else: 67 | r = np.identity(3) 68 | 69 | if angle_y != 0: 70 | c, s = cos(angle_y), sin(angle_y) 71 | r = r.dot(np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])) 72 | 73 | if angle_z != 0: 74 | c, s = cos(angle_z), sin(angle_z) 75 | r = r.dot(np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])) 76 | 77 | return r 78 | 79 | 80 | def rotate2(x, angle, origin=(0, 0)): 81 | """ 82 | Rotates the input 2D vectors by the specified angle. 83 | 84 | Parameters 85 | ---------- 86 | x : array 87 | One or multiple vectors to rotate. 88 | angle : float 89 | The counter clockwise rotation angle in radians. 90 | origin : array 91 | The point about which the rotation will be performed. 92 | """ 93 | origin = np.asarray(origin) 94 | x = np.asarray(x) - origin 95 | r = rotation_matrix2(angle) 96 | return x.dot(r.T) + origin 97 | 98 | 99 | def rotate3(x, angle_x=0, angle_y=0, angle_z=0, origin=(0, 0, 0)): 100 | """ 101 | Rotates the input 3D vectors by the specified angles. 102 | 103 | Parameters 104 | ---------- 105 | x : array 106 | One or multiple vectors to rotate. 107 | angle_x, angle_y, angle_z : float 108 | The counter clockwise rotation angles about the x, y, and z axes 109 | in radians. 110 | origin : array 111 | The point about which the rotation will be performed. 112 | """ 113 | origin = np.asarray(origin) 114 | x = np.asarray(x) - origin 115 | r = rotation_matrix3(angle_x, angle_y, angle_z) 116 | return x.dot(r.T) + origin 117 | -------------------------------------------------------------------------------- /civpy/math/linalg_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from .linalg import * 4 | 5 | 6 | def test_projection_angles(): 7 | i = np.identity(3) 8 | 9 | a = np.abs(rotate3(i, *projection_angles('xy')).ravel()) 10 | b = i.ravel() 11 | assert pytest.approx(a) == b 12 | 13 | a = np.abs(rotate3(i, *projection_angles('xz')).ravel()) 14 | b = np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]]).ravel() 15 | assert pytest.approx(a) == b 16 | 17 | a = np.abs(rotate3(i, *projection_angles('yz')).ravel()) 18 | b = np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]]).ravel() 19 | assert pytest.approx(a) == b 20 | 21 | a = np.abs(rotate3(i, *projection_angles('yx')).ravel()) 22 | b = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]]).ravel() 23 | assert pytest.approx(a) == b 24 | 25 | a = np.abs(rotate3(i, *projection_angles('zx')).ravel()) 26 | b = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]]).ravel() 27 | assert pytest.approx(a) == b 28 | 29 | a = np.abs(rotate3(i, *projection_angles('zy')).ravel()) 30 | b = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]).ravel() 31 | assert pytest.approx(a) == b 32 | 33 | with pytest.raises(ValueError): 34 | projection_angles('xyz') 35 | 36 | 37 | def test_rotate2(): 38 | v = np.array([[1, 0], [0, 1]]) 39 | a = rotate2(v, np.pi/2).ravel() 40 | b = np.array([[0, 1], [-1, 0]]).ravel() 41 | assert pytest.approx(a) == b 42 | 43 | v = np.array([1, 0]) 44 | a = rotate2(v, np.pi/2) 45 | b = np.array([0, 1]) 46 | assert pytest.approx(a) == b 47 | 48 | 49 | def test_rotate3(): 50 | v = np.array([1, 0, 0]) 51 | 52 | a = rotate3(v, angle_z=np.pi/2) 53 | b = np.array([0, 1, 0]) 54 | assert pytest.approx(a) == b 55 | 56 | a = rotate3(v, angle_x=np.pi/2) 57 | b = np.array([1, 0, 0]) 58 | assert pytest.approx(a) == b 59 | 60 | a = rotate3(v, angle_y=-np.pi/2) 61 | b = np.array([0, 0, 1]) 62 | assert pytest.approx(a) == b 63 | -------------------------------------------------------------------------------- /civpy/math/optimize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import scipy.optimize 6 | 7 | __all__ = ['fsolve'] 8 | 9 | 10 | def fsolve(*args, **kwargs): 11 | """ 12 | Finds the roots of a function. If the function fails to find a solution, 13 | an exception is raised. See :func:`scipy.optimize.fsolve` for list of 14 | parameters. 15 | """ 16 | kwargs['full_output'] = True 17 | x, infodict, ier, mesg = scipy.optimize.fsolve(*args, **kwargs) 18 | 19 | if ier != 1: 20 | raise ValueError('{}\n{}'.format(mesg, infodict)) 21 | 22 | return x 23 | -------------------------------------------------------------------------------- /civpy/math/optimize_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from .optimize import * 4 | 5 | 6 | def test_fsolve(): 7 | def func1(x): 8 | return x**2 + 1 9 | 10 | def func2(x): 11 | return x**2 - 1 12 | 13 | with pytest.raises(ValueError): 14 | fsolve(func1, [0]) 15 | 16 | a = fsolve(func2, [-10, 10]) 17 | b = np.array([-1, 1]) 18 | assert pytest.approx(a) == b 19 | -------------------------------------------------------------------------------- /civpy/structures/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==================================== 3 | Structures (:mod:`civpy.structures`) 4 | ==================================== 5 | 6 | Contains components for performing structural analysis. 7 | 8 | Components 9 | ========== 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | CrossSection 14 | Material 15 | ElementGroup 16 | Node 17 | Element 18 | Structure 19 | 20 | 21 | Loads 22 | ===== 23 | .. autosummary:: 24 | :toctree: generated/ 25 | 26 | LoadCase 27 | NodeLoad 28 | ElementLoad 29 | 30 | 31 | Element Functions 32 | ================= 33 | .. autosummary:: 34 | :toctree: generated/ 35 | 36 | rotation_matrix 37 | transformation_matrix 38 | local_stiffness 39 | clear_element_cache 40 | 41 | 42 | Element Load Functions 43 | ====================== 44 | .. autosummary:: 45 | :toctree: generated/ 46 | 47 | load_distances 48 | force_local_reactions 49 | moment_local_reactions 50 | local_reactions 51 | clear_element_load_cache 52 | """ 53 | 54 | from xsect import CrossSection 55 | from .element_group import * 56 | from .element_load import * 57 | from .element import * 58 | from .load_case import * 59 | from .material import * 60 | from .node_load import * 61 | from .node import * 62 | from .structure import * 63 | -------------------------------------------------------------------------------- /civpy/structures/element.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import copy 6 | import attr 7 | import weakref 8 | import numpy as np 9 | from math import cos, sin 10 | from functools import lru_cache 11 | 12 | __all__ = [ 13 | 'rotation_matrix', 14 | 'transformation_matrix', 15 | 'local_stiffness', 16 | 'clear_element_cache', 17 | 'Element', 18 | ] 19 | 20 | 21 | def rotation_matrix(dx, dy, dz, roll=0): 22 | """ 23 | Returns the rotation matrix of shape (3, 3) for an element. 24 | 25 | Parameters 26 | ---------- 27 | dx, dy, dz : float 28 | The distance changes from the i to j end of the element. 29 | roll : float 30 | The roll of the element counter clockwise about its axis. 31 | """ 32 | l = (dx**2 + dy**2 + dz**2)**0.5 33 | rx, ry, rz = dx / l, dy / l, dz / l 34 | c, s = cos(-roll), sin(-roll) 35 | 36 | if rx == 0 and rz == 0: 37 | r = [[0, ry, 0], [-ry*c, 0, s], [ry*s, 0, c]] 38 | else: 39 | rxz = (rx**2 + rz**2)**0.5 40 | r = [[rx, ry, rz], 41 | [(-rx*ry*c - rz*s)/rxz, rxz*c, (-ry*rz*c + rx*s)/rxz], 42 | [(rx*ry*s - rz*c)/rxz, -rxz*s, (ry*rz*s + rx*c)/rxz]] 43 | 44 | return np.array(r, dtype='float') 45 | 46 | 47 | @lru_cache(maxsize=1000) 48 | def transformation_matrix(dx, dy, dz, roll=0): 49 | """ 50 | Returns the transformation matrix of shape (12, 12) for an element. 51 | 52 | Parameters 53 | ---------- 54 | dx, dy, dz : float 55 | The distance changes from the i to j end of the element. 56 | roll : float 57 | The roll of the element counter clockwise about its axis. 58 | """ 59 | r = rotation_matrix(dx, dy, dz, roll) 60 | t = np.zeros((12, 12), dtype='float') 61 | t[:3,:3] = t[3:6,3:6] = t[6:9,6:9] = t[9:12,9:12] = r 62 | return t 63 | 64 | 65 | @lru_cache(maxsize=1000) 66 | def local_stiffness(l, lu, a, ix, iy, j, e, g, 67 | imx_free=False, imy_free=False, imz_free=False, 68 | jmx_free=False, jmy_free=False, jmz_free=False): 69 | """ 70 | Returns the local stiffness matrix of shape (12, 12) of the element. 71 | 72 | Parameters 73 | ---------- 74 | l : float 75 | The length of the element. 76 | lu : float 77 | The unstressed length of the element. 78 | a : float 79 | The cross sectional area of the element. 80 | ix, iy : float 81 | The moment of inertias about the local x and y axes of the element. 82 | j : float 83 | The polar moment of inertia of the element. 84 | e : float 85 | The modulus of elasticity of the element. 86 | g : float 87 | The modulus of rigidity of the element. 88 | imx_free, imy_free, imz_free : bool 89 | The i-end releases for the element. 90 | jmx_free, jmy_free, jmz_free : bool 91 | The j-end releases for the element. 92 | """ 93 | k = np.zeros((12, 12), dtype='float') 94 | iy, iz = ix, iy 95 | f = e / l**3 96 | 97 | k[0, 0] = k[6, 6] = a*e/lu 98 | k[0, 6] = k[6, 0] = -k[0, 0] 99 | 100 | if not imz_free or not jmz_free: 101 | k[3, 3] = k[9, 9] = g*j/l 102 | k[9, 3] = k[3, 9] = -k[3, 3] 103 | 104 | if not imy_free: 105 | if not jmy_free: 106 | # Fixed-Fixed 107 | k[1, 1] = k[7, 7] = f*12*iz 108 | k[1, 7] = k[7, 1] = -k[1, 1] 109 | k[5, 5] = k[11, 11] = f*4*l**2*iz 110 | k[5, 11] = k[11, 5] = f*2*l**2*iz 111 | k[1, 5] = k[1, 11] = k[5, 1] = k[11 ,1] = f*6*l*iz 112 | k[5, 7] = k[7, 5] = k[7, 11] = k[11, 7] = -k[1, 5] 113 | else: 114 | # Fixed-Free 115 | k[1, 1] = k[7, 7] = f*3*iz 116 | k[1, 7] = k[7, 1] = -k[1, 1] 117 | k[1, 5] = k[5, 1] = f*3*l*iz 118 | k[5, 7] = k[7, 5] = -k[1, 5] 119 | k[5, 5] = f*3*l**2*iz 120 | elif not jmy_free: 121 | # Free-Fixed 122 | k[1, 1] = k[7, 7] = f*3*iz 123 | k[1, 7] = k[7, 1] = -k[1, 1] 124 | k[1, 11] = k[11 ,1] = f*3*l*iz 125 | k[7, 11] = k[11, 7] = -k[1, 11] 126 | k[11, 11] = f*3*l**2*iz 127 | 128 | if not imx_free: 129 | if not jmx_free: 130 | # Fixed-Fixed 131 | k[2, 2] = k[8, 8] = f*12*iy 132 | k[2, 8] = k[8, 2] = -k[2, 2] 133 | k[4, 8] = k[8, 4] = k[10, 8] = k[8, 10] = f*6*l*iy 134 | k[2, 4] = k[2, 10] = k[4, 2] = k[10, 2] = -k[4, 8] 135 | k[4, 4] = k[10, 10] = f*4*l**2*iy 136 | k[4, 10] = k[10, 4] = f*2*l**2*iy 137 | else: 138 | # Fixed-Free 139 | k[2, 2] = k[8, 8] = f*3*iy 140 | k[2, 8] = k[8, 2] = -k[2, 2] 141 | k[4, 8] = k[8, 4] = f*3*l*iy 142 | k[2, 4] = k[4, 2] = -k[4, 8] 143 | k[4, 4] = 3*l**2*iy 144 | elif not jmx_free: 145 | # Free-Fixed 146 | k[2, 2] = k[8, 8] = f*3*iy 147 | k[2, 8] = k[8, 2] = -k[2, 2] 148 | k[8, 10] = k[10, 8] = f*3*l*iy 149 | k[2, 10] = k[10, 2] = -k[8, 10] 150 | k[10, 10] = f*3*l**2*iy 151 | 152 | return k 153 | 154 | 155 | def clear_element_cache(): 156 | """Clears the element function cache.""" 157 | local_stiffness.cache_clear() 158 | transformation_matrix.cache_clear() 159 | 160 | 161 | @attr.s(hash=False) 162 | class Element(object): 163 | """ 164 | A class representing a structural element. 165 | 166 | Parameters 167 | ---------- 168 | name : str 169 | A unique name for the element. 170 | inode, jnode : str 171 | The names of the nodes at the i and j ends of the element. 172 | group : :class:`.ElementGroup` 173 | The group assigned to the element. 174 | symmetry : {None, 'x', 'y', 'xy'} 175 | The symmetry of the element. 176 | roll : float 177 | The counter clockwise angle of roll about the length axis. 178 | imx_free, imy_free, imz_free : bool 179 | The rotational fixities at the i-node about the local x, y, and z axes. 180 | jmx_free, jmy_free, jmz_free : bool 181 | The rotational fixities at the j-node about the local x, y, and z axes. 182 | """ 183 | P = 'p' 184 | X = 'x' 185 | Y = 'y' 186 | XY = 'xy' 187 | SYMMETRIES = (None, X, Y, XY) 188 | 189 | TRANSFORMS = { 190 | X: {P: X, X: P, Y: XY, XY: Y}, 191 | Y: {P: Y, X: XY, Y: P, XY: X}, 192 | } 193 | 194 | name = attr.ib(converter=str) 195 | inode = attr.ib() 196 | jnode = attr.ib() 197 | group = attr.ib() 198 | symmetry = attr.ib(default=None, validator=attr.validators.in_(SYMMETRIES)) 199 | roll = attr.ib(default=0) 200 | unstr_length = attr.ib(default=None) 201 | imx_free = attr.ib(default=False) 202 | imy_free = attr.ib(default=False) 203 | imz_free = attr.ib(default=False) 204 | jmx_free = attr.ib(default=False) 205 | jmy_free = attr.ib(default=False) 206 | jmz_free = attr.ib(default=False) 207 | _inode_ref = attr.ib(default=None, init=False, repr=False) 208 | _jnode_ref = attr.ib(default=None, init=False, repr=False) 209 | 210 | def inode_ref(): 211 | def fget(self): 212 | value = self._inode_ref 213 | if value is None: 214 | return value 215 | return value() 216 | def fset(self, value): 217 | if value is not None: 218 | value = weakref.ref(value) 219 | self._inode_ref = value 220 | def fdel(self): 221 | del self._inode_ref 222 | return locals() 223 | inode_ref = property(**inode_ref()) 224 | 225 | def jnode_ref(): 226 | def fget(self): 227 | value = self._jnode_ref 228 | if value is None: 229 | return value 230 | return value() 231 | def fset(self, value): 232 | if value is not None: 233 | value = weakref.ref(value) 234 | self._jnode_ref = value 235 | def fdel(self): 236 | del self._jnode_ref 237 | return locals() 238 | jnode_ref = property(**jnode_ref()) 239 | 240 | def __str__(self): 241 | return self.name 242 | 243 | def copy(self): 244 | """Returns a copy of the element.""" 245 | return copy.copy(self) 246 | 247 | def i_free(self): 248 | """Sets the i end rotational fixities to free. Returns the element.""" 249 | self.imx_free = self.imy_free = self.imz_free = True 250 | return self 251 | 252 | def j_free(self): 253 | """Sets the j end rotational fixities to free. Returns the element.""" 254 | self.jmx_free = self.jmy_free = self.jmz_free = True 255 | return self 256 | 257 | def free(self): 258 | """Sets the end rotational fixities to free. Returns the element.""" 259 | return self.i_free().j_free() 260 | 261 | def mx_free(self): 262 | """Sets the x rotational fixities to free. Returns the element.""" 263 | self.imx_free = self.jmx_free = True 264 | return self 265 | 266 | def my_free(self): 267 | """Sets the y rotational fixities to free. Returns the element.""" 268 | self.imy_free = self.jmy_free = True 269 | return self 270 | 271 | def mz_free(self): 272 | """Sets the z rotational fixities to free. Returns the element.""" 273 | self.imz_free = self.jmz_free = True 274 | return self 275 | 276 | def set_nodes(self, ndict): 277 | """ 278 | Sets the node references for the element. 279 | 280 | Parameters 281 | ---------- 282 | ndict : dict 283 | A dictionary that maps node names to :class:`.Node` objects. 284 | """ 285 | self.inode_ref = ndict[self.inode] 286 | self.jnode_ref = ndict[self.jnode] 287 | 288 | def get_nodes(self): 289 | """Returns the i and j node objects.""" 290 | if self.inode_ref is None or self.jnode_ref is None: 291 | raise ValueError('Node references have not been set.') 292 | return self.inode_ref, self.jnode_ref 293 | 294 | def get_unstr_length(self): 295 | """ 296 | If the unstressed length of the element is None, returns the initial 297 | distance between the nodes. If the unstressed length is a string, 298 | converts the string to a float and adds it to the initial distance 299 | between the nodes. Otherwise, returns the assigned unstressed length. 300 | """ 301 | if self.unstr_length is None: 302 | return self.length() 303 | 304 | elif isinstance(self.unstr_length, str): 305 | return self.length() + float(self.unstr_length) 306 | 307 | return self.unstr_length 308 | 309 | def length(self, di=(0, 0, 0), dj=(0, 0, 0)): 310 | """ 311 | Returns the length of the element between nodes. 312 | 313 | Parameters 314 | ---------- 315 | di, dj : array 316 | The deflections at the i and j ends of the element. 317 | """ 318 | xi, xj = self.get_nodes() 319 | di, dj = np.asarray(di), np.asarray(dj) 320 | delta = (xj - xi) + (dj - di) 321 | return np.linalg.norm(delta) 322 | 323 | def sym_elements(self): 324 | """Returns the symmetric elements for the element.""" 325 | def trans(name, *sym): 326 | t = Element.TRANSFORMS 327 | n = name.split('_') 328 | 329 | for x in sym: 330 | n[-1] = t[x][n[-1]] 331 | 332 | return '_'.join(n) 333 | 334 | def primary(): 335 | e = self.copy() 336 | e.name = '{}_p'.format(self.name) 337 | return e 338 | 339 | def x_sym(): 340 | e = self.copy() 341 | e.name = '{}_x'.format(self.name) 342 | e.inode = trans(self.inode, 'x') 343 | e.jnode = trans(self.jnode, 'x') 344 | return e 345 | 346 | def y_sym(): 347 | e = self.copy() 348 | e.name = '{}_y'.format(self.name) 349 | e.inode = trans(self.inode, 'y') 350 | e.jnode = trans(self.jnode, 'y') 351 | return e 352 | 353 | def xy_sym(): 354 | e = self.copy() 355 | e.name = '{}_xy'.format(self.name) 356 | e.inode = trans(self.inode, 'x', 'y') 357 | e.jnode = trans(self.jnode, 'x', 'y') 358 | return e 359 | 360 | if self.symmetry is None: 361 | return primary(), 362 | 363 | elif self.symmetry == 'x': 364 | return primary(), x_sym() 365 | 366 | elif self.symmetry == 'y': 367 | return primary(), y_sym() 368 | 369 | elif self.symmetry == 'xy': 370 | return primary(), x_sym(), y_sym(), xy_sym() 371 | 372 | def rotation_matrix(self, di=(0, 0, 0), dj=(0, 0, 0)): 373 | """ 374 | Returns the rotation matrix for the element. 375 | 376 | Parameters 377 | ---------- 378 | di, dj : array 379 | The deflections at the i and j ends of the element. 380 | """ 381 | xi, xj = self.get_nodes() 382 | di, dj = np.asarray(di), np.asarray(dj) 383 | dx, dy, dz = (xj - xi) + (dj - di) 384 | return rotation_matrix(dx, dy, dz, self.roll) 385 | 386 | def transformation_matrix(self, di=(0, 0, 0), dj=(0, 0, 0)): 387 | """ 388 | Returns the transformation matrix for the element. 389 | 390 | Parameters 391 | ---------- 392 | di, dj : array 393 | The deflections at the i and j ends of the element. 394 | """ 395 | xi, xj = self.get_nodes() 396 | di, dj = np.asarray(di), np.asarray(dj) 397 | dx, dy, dz = (xj - xi) + (dj - di) 398 | return transformation_matrix(dx, dy, dz, self.roll) 399 | 400 | def local_stiffness(self, di=(0, 0, 0), dj=(0, 0, 0)): 401 | """ 402 | Returns the local stiffness for the element. 403 | 404 | Parameters 405 | ---------- 406 | di, dj : array 407 | The deflections at the i and j ends of the element. 408 | """ 409 | group = self.group 410 | sect = group.section 411 | mat = group.material 412 | 413 | return local_stiffness( 414 | l=self.length(di, dj), 415 | lu=self.get_unstr_length(), 416 | a=sect.area, 417 | ix=sect.inertia_x, 418 | iy=sect.inertia_y, 419 | j=sect.inertia_j, 420 | e=mat.elasticity, 421 | g=mat.rigidity, 422 | imx_free=self.imx_free, 423 | imy_free=self.imy_free, 424 | imz_free=self.imz_free, 425 | jmx_free=self.jmx_free, 426 | jmy_free=self.jmy_free, 427 | jmz_free=self.jmz_free 428 | ) 429 | 430 | def global_stiffness(self, di=(0, 0, 0), dj=(0, 0, 0)): 431 | """ 432 | Returns the global stiffness matrix for the element. 433 | 434 | Parameters 435 | ---------- 436 | di, dj : array 437 | The deflections at the i and j ends of the element. 438 | """ 439 | di, dj = np.asarray(di), np.asarray(dj) 440 | t = self.transformation_matrix(di, dj) 441 | k = self.local_stiffness(di, dj) 442 | return t.T.dot(k).dot(t) 443 | -------------------------------------------------------------------------------- /civpy/structures/element_group.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import attr 6 | 7 | __all__ = ['ElementGroup'] 8 | 9 | 10 | @attr.s(hash=False) 11 | class ElementGroup(object): 12 | """ 13 | A class representing a group of element properties. 14 | 15 | Parameters 16 | ---------- 17 | name : str 18 | The name of the group. 19 | section : :class:`.CrossSection` 20 | The group cross section. 21 | material : :class:`.Material` 22 | The group material. 23 | """ 24 | name = attr.ib() 25 | section = attr.ib() 26 | material = attr.ib() 27 | -------------------------------------------------------------------------------- /civpy/structures/element_load.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import weakref 6 | import numpy as np 7 | from functools import lru_cache 8 | from .element import transformation_matrix 9 | 10 | __all__ = [ 11 | 'load_distances', 12 | 'force_local_reactions', 13 | 'moment_local_reactions', 14 | 'local_reactions', 15 | 'clear_element_load_cache', 16 | 'ElementLoad', 17 | ] 18 | 19 | 20 | def load_distances(dx, dy, dz, ix, delx): 21 | """ 22 | Returns the load distances to where an element load is applied. 23 | 24 | Parameters 25 | ---------- 26 | dx, dy, dz : float 27 | The element distance vector. 28 | ix, : float 29 | The distance from the i node of the element to where the beginning 30 | of the loads are applied. 31 | dx : float 32 | The distance from the ix position toward the j node over which 33 | the loads are applied. 34 | """ 35 | l = (dx**2 + dy**2 + dz**2)**0.5 36 | 37 | l1 = l * abs(ix) if ix < 0 else ix 38 | l2 = l * abs(delx) if delx < 0 else delx 39 | l2 = l - l1 - l2 40 | 41 | if l1 > l or l1 < 0 or l2 > l or l2 < 0: 42 | raise ValueError('Load applied beyond element bounds.') 43 | 44 | return l, l1, l2 45 | 46 | 47 | def force_local_reactions(fx, fy, fz, dx, dy, dz, roll, ix, delx): 48 | """ 49 | Returns the local force reaction vector for an element. 50 | 51 | Parameters 52 | ---------- 53 | fx, fy, fz : float 54 | The force vector. 55 | dx, dy, dz : float 56 | The element distance vector. 57 | roll : float 58 | The roll of the element. 59 | ix, : float 60 | The distance from the i node of the element to where the beginning 61 | of the loads are applied. 62 | dx : float 63 | The distance from the ix position toward the j node over which 64 | the loads are applied. 65 | """ 66 | l, l1, l2 = load_distances(dx, dy, dz, ix, delx) 67 | 68 | t = transformation_matrix(dx, dy, dz, roll) 69 | 70 | if delx == 0: 71 | # Point load 72 | fsi = (l2**2 / l**3) * (3*l1 + l2) 73 | fmi = l1*l2**2 / l**2 74 | fsj = (l1**2 / l**3) * (l1 + 3*l2) 75 | fmj = -fmi 76 | fti = ftj = 0 77 | fai = l2 / l 78 | faj = l1 / l 79 | else: 80 | # Uniform load 81 | fsi = (l / 2) * (1 - (2*l**3 - 2*l1**2*l + l1**3)*l1/l**4 - (2*l - l2)*l2**3/l**4) 82 | fmi = (l**2 / 12) * (1 - (6*l**2 - 8*l1*l + 3*l1**2)*l1**2/l**4 - (4*l - 3*l2)*l2**3/l**4) 83 | fsj = (l / 2) * (1 - (2*l - l1)*l1**3/l**4 - (2*l**3 - 2*l2**2*l + l2**3)*l2/l**4) 84 | fmj = -(l**2 / 12) * (1 - (4*l - 3*l1)*l1**3/l**4 - (6*l**2 - 8*l2*l + 3*l2**2)*l2**2/l**4) 85 | fti = ftj = 0 86 | fai = (l / 2) * (l - l1 - l2) * (l - l1 + l2) 87 | faj = -fai 88 | 89 | fx, fy, fz = t[:3,:3].dot([fx, fy, fz]) 90 | 91 | r = [-fx*fai, -fy*fsi, -fz*fsi, -fti, -fz*fmi, -fy*fmi, 92 | -fx*faj, -fy*fsj, -fz*fsj, -ftj, -fz*fmj, -fy*fmj] 93 | 94 | return np.array(r, dtype='float') 95 | 96 | 97 | def moment_local_reactions(mx, my, mz, dx, dy, dz, roll, ix, delx): 98 | """ 99 | Returns the local moment reaction vector for an element. 100 | 101 | Parameters 102 | ---------- 103 | mx, my, mz : float 104 | The moment vector. 105 | dx, dy, dz : float 106 | The element distance vector. 107 | roll : float 108 | The roll of the element. 109 | ix, : float 110 | The distance from the i node of the element to where the beginning 111 | of the loads are applied. 112 | dx : float 113 | The distance from the ix position toward the j node over which 114 | the loads are applied. 115 | """ 116 | l, l1, l2 = load_distances(dx, dy, dz, ix, delx) 117 | 118 | t = transformation_matrix(dx, dy, dz, roll) 119 | 120 | if delx == 0: 121 | # Point load 122 | fsi = -6*l1*l2 / l**3 123 | fmi = (l2 / l**2) * (l2 - 2*l1) 124 | fsj = -fsi 125 | fmj = (l1 / l**2) * (l1 - 2*l2) 126 | fti = l2 / l 127 | ftj = l1 / l 128 | fai = faj = 0 129 | else: 130 | # Uniform load 131 | fsi = 2*((l-l2)**3 - l1**3)/l**3 - 3*((l-l2)**2 - l1**2)/l**2 132 | fmi = ((l-l2) - l1) - 2*((l-l2)**2 - l1**2)/l + ((l-l2)**3 - l1**3)/l**2 133 | fsj = -fsi 134 | fmj = ((l-l2)**3 - l1**3)/l**2 - ((l-l2)**2 - l1**2)/l 135 | fti = ((l-l2) - l1) - ((l-l2)**2 - l1**2)/(2*l) 136 | ftj = ((l-l2)**2 - l1**2)/(2*l) 137 | fai = faj = 0 138 | 139 | mx, my, mz = t[:3,:3].dot([mx, my, mz]) 140 | 141 | r = [-fai, -my*fsi, -mx*fsi, -mx*fti, -my*fmi, -mz*fmi, 142 | -faj, -my*fsj, -mx*fsj, -mx*ftj, -my*fmj, -mz*fmj] 143 | 144 | return np.array(r, dtype='float') 145 | 146 | 147 | @lru_cache(maxsize=1000) 148 | def local_reactions(fx, fy, fz, mx, my, mz, dx, dy, dz, roll, ix, delx, 149 | imx_free, imy_free, imz_free, jmx_free, jmy_free, jmz_free): 150 | """ 151 | Returns the local reaction vector for an element. 152 | 153 | Parameters 154 | ---------- 155 | fx, fy, fz : float 156 | The force vector. 157 | mx, my, mz : float 158 | The moment vector. 159 | dx, dy, dz : float 160 | The element distance vector. 161 | roll : float 162 | The roll of the element. 163 | ix, : float 164 | The distance from the i node of the element to where the beginning 165 | of the loads are applied. 166 | dx : float 167 | The distance from the ix position toward the j node over which 168 | the loads are applied. 169 | imx_free, imy_free, imz_free : bool 170 | The fixities at the i end of the element. 171 | jmx_free, jmy_free, jmz_free : bool 172 | The fixities at the j end of the element. 173 | """ 174 | r = force_local_reactions(fx, fy, fz, dx, dy, dz, roll, ix, delx) 175 | r += moment_local_reactions(mx, my, mz, dx, dy, dz, roll, ix, delx) 176 | 177 | # Adjust reactions for element end fixities 178 | if imz_free: 179 | if not jmz_free: 180 | # Free-Fixed 181 | r[9] += r[3] 182 | r[3] = 0 183 | else: 184 | # Free-Free 185 | r[3] = r[9] = 0 186 | elif jmz_free: 187 | # Fixed-Free 188 | r[3] += r[9] 189 | r[9] = 0 190 | 191 | if imx_free: 192 | if not jmx_free: 193 | # Free-Fixed 194 | r[1] -= 1.5 * r[5] / l 195 | r[7] += 1.5 * r[5] / l 196 | r[11] -= 0.5 * r[5] 197 | r[5] = 0 198 | else: 199 | # Free-Free 200 | r[1] -= (r[5] + r[11]) / l 201 | r[7] += (r[5] + r[11]) / l 202 | r[5] = r[11] = 0 203 | elif jmx_free: 204 | # Fixed-Free 205 | r[1] -= 1.5 * r[11] / l 206 | r[5] -= 0.5 * r[11] 207 | r[7] += 1.5 * r[11] / l 208 | r[11] = 0 209 | 210 | if imy_free: 211 | if not jmy_free: 212 | # Free-Fixed 213 | r[2] += 1.5 * r[4] / l 214 | r[8] -= 1.5 * r[4] / l 215 | r[10] -= 0.5 * r[4] / l 216 | r[4] = 0 217 | else: 218 | # Free-Free 219 | r[2] += (r[4] + r[10]) / l 220 | r[8] -= (r[4] + r[10]) / l 221 | r[4] = r[10] = 0 222 | elif jmy_free: 223 | # Fixed-Free 224 | r[2] += 1.5 * r[10] / l 225 | r[4] -= 0.5 * r[10] 226 | r[8] -= 1.5 * r[10] / l 227 | r[10] = 0 228 | 229 | return r 230 | 231 | 232 | def clear_element_load_cache(): 233 | """Clears the element load function cache.""" 234 | local_reactions.cache_clear() 235 | 236 | 237 | class ElementLoad(np.ndarray): 238 | """ 239 | A class representing an element load. 240 | 241 | Parameters 242 | ---------- 243 | element : str 244 | The name of the element to which the loads are applied. 245 | fx, fy, fz : float 246 | The global forces applied to the element. 247 | mx, my, mz : float 248 | The global moments applied to the element. 249 | ix, : float 250 | The distance from the i node at where the loads are applied. 251 | dx : float 252 | The distance from the ix position toward the j node over which 253 | the loads are applied. 254 | """ 255 | def __new__(cls, element, fx=0, fy=0, fz=0, mx=0, my=0, mz=0, ix=0, dx=-1): 256 | obj = np.array([fx, fy, fz, mx, my, mz], dtype='float').view(cls) 257 | obj.element = element 258 | obj.ix = ix 259 | obj.dx = dx 260 | return obj 261 | 262 | def __array_finalize__(self, obj): 263 | if obj is None: return 264 | self.element = getattr(obj, 'element', '') 265 | self.ix = getattr(obj, 'ix', 0) 266 | self.dx = getattr(obj, 'dx', 0) 267 | self.element_ref = None 268 | 269 | def element(): 270 | def fget(self): 271 | return self._element 272 | def fset(self, value): 273 | if not isinstance(value, str): 274 | value = str(value) 275 | self._element = value 276 | def fdel(self): 277 | del self._element 278 | return locals() 279 | element = property(**element()) 280 | 281 | def element_ref(): 282 | def fget(self): 283 | value = self._element_ref 284 | if value is None: 285 | return value 286 | return value() 287 | def fset(self, value): 288 | if value is not None: 289 | value = weakref.ref(value) 290 | self._element_ref = value 291 | def fdel(self): 292 | del self._element_ref 293 | return locals() 294 | element_ref = property(**element_ref()) 295 | 296 | def fx(): 297 | def fget(self): 298 | return self[0] 299 | def fset(self, value): 300 | self[0] = value 301 | return locals() 302 | fx = property(**fx()) 303 | 304 | def fy(): 305 | def fget(self): 306 | return self[1] 307 | def fset(self, value): 308 | self[1] = value 309 | return locals() 310 | fy = property(**fy()) 311 | 312 | def fz(): 313 | def fget(self): 314 | return self[2] 315 | def fset(self, value): 316 | self[2] = value 317 | return locals() 318 | fz = property(**fz()) 319 | 320 | def mx(): 321 | def fget(self): 322 | return self[3] 323 | def fset(self, value): 324 | self[3] = value 325 | return locals() 326 | mx = property(**mx()) 327 | 328 | def my(): 329 | def fget(self): 330 | return self[4] 331 | def fset(self, value): 332 | self[4] = value 333 | return locals() 334 | my = property(**my()) 335 | 336 | def mz(): 337 | def fget(self): 338 | return self[5] 339 | def fset(self, value): 340 | self[5] = value 341 | return locals() 342 | mz = property(**mz()) 343 | 344 | def __repr__(self): 345 | s = [ 346 | 'element={!r}'.format(self.element), 347 | 'forces={!r}'.format((self.fx, self.fy, self.fz)), 348 | 'moments={!r}'.format((self.mx, self.my, self.mz)), 349 | 'ix={!r}'.format(self.ix), 350 | 'dx={!r}'.format(self.dx), 351 | ] 352 | 353 | return '{}({})'.format(type(self).__name__, ', '.join(s)) 354 | 355 | def forces(self): 356 | """Returns the force vector.""" 357 | return self[:3] 358 | 359 | def moments(self): 360 | """Returns the moment vector.""" 361 | return self[3:6] 362 | 363 | def get_element(self): 364 | """Gets the referenced element.""" 365 | if self._element_ref is None: 366 | raise ValueError('Element has not been set.') 367 | return self._element_ref 368 | 369 | def set_element(self, edict): 370 | """ 371 | Sets the element reference. 372 | 373 | Parameters 374 | ---------- 375 | edict : dict 376 | A dictionary mapping node names to node objects. 377 | """ 378 | self._element_ref = edict[self.element] 379 | 380 | def local_reactions(self, di=(0, 0, 0), dj=(0, 0, 0)): 381 | """ 382 | Returns the local end reactions for the element. 383 | 384 | Parameters 385 | ---------- 386 | di, dj : array 387 | The deflections at the i and j ends of the element. 388 | """ 389 | di, dj = np.asarray(di), np.asarray(dj) 390 | e = self.get_element() 391 | xi, xj = e.get_nodes() 392 | 393 | dx, dy, dz = (xj - xi) + (dj - di) 394 | fx, fy, fz = self.forces() 395 | mx, my, mz = self.moments() 396 | 397 | r = local_reactions( 398 | fx, fy, fz, mx, my, mz, dx, dy, dz, 399 | e.roll, self.ix, self.dx, 400 | e.imx_free, e.imy_free, e.imz_free, 401 | e.jmx_free, e.jmy_free, e.jmz_free 402 | ) 403 | 404 | return r 405 | 406 | def global_reactions(self, di=(0, 0, 0), dj=(0, 0, 0)): 407 | """ 408 | Returns the global end reactions for the element. 409 | 410 | Parameters 411 | ---------- 412 | di, dj : array 413 | The deflections at the i and j ends of the element. 414 | """ 415 | di, dj = np.asarray(di), np.asarray(dj) 416 | e = self.get_element() 417 | t = e.transformation_matrix(di, dj) 418 | q = self.local_reactions(di, dj) 419 | return t.T.dot(q) 420 | -------------------------------------------------------------------------------- /civpy/structures/element_load_test.py: -------------------------------------------------------------------------------- 1 | from .element_load import * 2 | 3 | 4 | def test_repr(): 5 | e = ElementLoad('1', fy=-0.25) 6 | repr(e) 7 | -------------------------------------------------------------------------------- /civpy/structures/element_test.py: -------------------------------------------------------------------------------- 1 | from .element import * 2 | 3 | 4 | def test_repr(): 5 | e = Element('1', 'a', 'b', None) 6 | repr(e) 7 | 8 | 9 | def test_str(): 10 | e = Element('1', 'a', 'b', None) 11 | assert str(e) == '1' 12 | 13 | 14 | def test_free(): 15 | e = Element('1', 'a', 'b', None).free() 16 | a = (e.imx_free, e.imy_free, e.imz_free, e.jmx_free, e.jmy_free, e.jmz_free) 17 | b = tuple([True] * 6) 18 | 19 | assert a == b 20 | 21 | 22 | def test_mx_free(): 23 | e = Element('1', 'a', 'b', None).mx_free() 24 | a = (e.imx_free, e.imy_free, e.imz_free, e.jmx_free, e.jmy_free, e.jmz_free) 25 | b = (True, False, False, True, False, False) 26 | assert a == b 27 | 28 | 29 | def test_my_free(): 30 | e = Element('1', 'a', 'b', None).my_free() 31 | a = (e.imx_free, e.imy_free, e.imz_free, e.jmx_free, e.jmy_free, e.jmz_free) 32 | b = (False, True, False, False, True, False) 33 | assert a == b 34 | 35 | 36 | def test_mz_free(): 37 | e = Element('1', 'a', 'b', None).mz_free() 38 | a = (e.imx_free, e.imy_free, e.imz_free, e.jmx_free, e.jmy_free, e.jmz_free) 39 | b = (False, False, True, False, False, True) 40 | assert a == b 41 | 42 | 43 | def test_copy(): 44 | e = Element('1', 'a', 'b', None) 45 | a = e.copy() 46 | 47 | assert e is not a 48 | assert repr(e) == repr(a) 49 | 50 | 51 | def test_sym_elements(): 52 | # No symmetry 53 | e = Element('1', 'a_p', 'a_x', None) 54 | p, = e.sym_elements() 55 | 56 | assert (p.name, p.inode, p.jnode) == ('1_p', 'a_p', 'a_x') 57 | 58 | # X symmetry 59 | e = Element('1', 'a_p', 'a_x', None, symmetry='x') 60 | p, x = e.sym_elements() 61 | 62 | assert (p.name, p.inode, p.jnode) == ('1_p', 'a_p', 'a_x') 63 | assert (x.name, x.inode, x.jnode) == ('1_x', 'a_x', 'a_p') 64 | 65 | # Y symmetry 66 | e = Element('1', 'a_p', 'a_x', None, symmetry='y') 67 | p, y = e.sym_elements() 68 | 69 | assert (p.name, p.inode, p.jnode) == ('1_p', 'a_p', 'a_x') 70 | assert (y.name, y.inode, y.jnode) == ('1_y', 'a_y', 'a_xy') 71 | 72 | # XY symmetry 73 | e = Element('1', 'a_p', 'a_x', None, symmetry='xy') 74 | p, x, y, xy = e.sym_elements() 75 | 76 | assert (p.name, p.inode, p.jnode) == ('1_p', 'a_p', 'a_x') 77 | assert (x.name, x.inode, x.jnode) == ('1_x', 'a_x', 'a_p') 78 | assert (y.name, y.inode, y.jnode) == ('1_y', 'a_y', 'a_xy') 79 | assert (xy.name, xy.inode, xy.jnode) == ('1_xy', 'a_xy', 'a_y') 80 | -------------------------------------------------------------------------------- /civpy/structures/load_case.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import attr 6 | 7 | __all__ = ['LoadCase'] 8 | 9 | 10 | @attr.s(hash=False) 11 | class LoadCase(object): 12 | """ 13 | A class representing a structural load case. 14 | 15 | Parameters 16 | ---------- 17 | name : str 18 | The name of the load case. 19 | node_loads : list 20 | A list of :class:`.NodeLoad` to apply with the load case. 21 | elem_loads : list 22 | A list of :class:`.ElementLoad` to apply with the load case. 23 | """ 24 | # Custom properties 25 | name = attr.ib() 26 | node_loads = attr.ib(default=[]) 27 | elem_loads = attr.ib(default=[]) 28 | 29 | def set_nodes(self, ndict): 30 | """ 31 | Sets the node references for all node loads assigned to the load case. 32 | 33 | Parameters 34 | ---------- 35 | ndict : dict 36 | A dictionary mapping node names to node objects. 37 | """ 38 | for n in self.node_loads: 39 | n.set_node(ndict) 40 | 41 | def set_elements(self, edict): 42 | """ 43 | Sets the element references for all element loads assigned to the load 44 | case. 45 | 46 | Parameters 47 | ---------- 48 | edict : dict 49 | A dictionary mapping element names to element objects. 50 | """ 51 | for e in self.elem_loads: 52 | e.set_element(edict) 53 | -------------------------------------------------------------------------------- /civpy/structures/load_case_test.py: -------------------------------------------------------------------------------- 1 | from .load_case import * 2 | 3 | 4 | def test_repr(): 5 | lc = LoadCase('dummy', None, None) 6 | repr(lc) 7 | -------------------------------------------------------------------------------- /civpy/structures/material.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import attr 6 | 7 | __all__ = ['Material'] 8 | 9 | 10 | @attr.s(hash=False) 11 | class Material(object): 12 | """ 13 | A class representing an engineered material. 14 | 15 | Parameters 16 | ---------- 17 | name : str 18 | The name of the material. 19 | elasticity : float 20 | The modulus of elasticity. 21 | rigidity : float 22 | The modulus of rigidity. 23 | """ 24 | name = attr.ib() 25 | elasticity = attr.ib() 26 | rigidity = attr.ib(default=0) 27 | -------------------------------------------------------------------------------- /civpy/structures/material_test.py: -------------------------------------------------------------------------------- 1 | from .material import * 2 | 3 | 4 | def test_repr(): 5 | m = Material('dummy', elasticity=29000, rigidity=11500) 6 | repr(m) 7 | -------------------------------------------------------------------------------- /civpy/structures/node.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import copy 6 | import numpy as np 7 | 8 | __all__ = ['Node'] 9 | 10 | 11 | class Node(np.ndarray): 12 | """ 13 | A class representing a structural node. 14 | 15 | Parameters 16 | ---------- 17 | name : str 18 | A unique name for the node. 19 | x, y, z : float 20 | The x, y, and z coordinates of the node. 21 | symmetry : {None, 'x', 'y', 'xy'} 22 | The symmetry of the node. 23 | fx_free, fy_free, fz_free : bool 24 | The force fixities of the node in the x, y, and z directions. 25 | mx_free, my_free, mz_free : bool 26 | The moment fixities of the node about the x, y, and z axes. 27 | """ 28 | X = 'x' 29 | Y = 'y' 30 | XY = 'xy' 31 | SYMMETRIES = (None, X, Y, XY) 32 | 33 | def __new__(cls, name, x=0, y=0, z=0, symmetry=None, 34 | fx_free=True, fy_free=True, fz_free=True, 35 | mx_free=True, my_free=True, mz_free=True): 36 | obj = np.array([x, y, z], dtype='float').view(cls) 37 | 38 | obj.name = name 39 | obj.symmetry = symmetry 40 | 41 | obj.fx_free = fx_free 42 | obj.fy_free = fy_free 43 | obj.fz_free = fz_free 44 | 45 | obj.mx_free = mx_free 46 | obj.my_free = my_free 47 | obj.mz_free = mz_free 48 | 49 | return obj 50 | 51 | def __array_finalize__(self, obj): 52 | if obj is None: return 53 | 54 | self.name = getattr(obj, 'name', '') 55 | self.symmetry = getattr(obj, 'symmetry', None) 56 | 57 | self.fx_free = getattr(obj, 'fx_free', True) 58 | self.fy_free = getattr(obj, 'fy_free', True) 59 | self.fz_free = getattr(obj, 'fz_free', True) 60 | 61 | self.mx_free = getattr(obj, 'mx_free', True) 62 | self.my_free = getattr(obj, 'my_free', True) 63 | self.mz_free = getattr(obj, 'mz_free', True) 64 | 65 | def x(): 66 | def fget(self): 67 | return self[0] 68 | def fset(self, value): 69 | self[0] = value 70 | return locals() 71 | x = property(**x()) 72 | 73 | def y(): 74 | def fget(self): 75 | return self[1] 76 | def fset(self, value): 77 | self[1] = value 78 | return locals() 79 | y = property(**y()) 80 | 81 | def z(): 82 | def fget(self): 83 | return self[2] 84 | def fset(self, value): 85 | self[2] = value 86 | return locals() 87 | z = property(**z()) 88 | 89 | def symmetry(): 90 | def fget(self): 91 | return self._symmetry 92 | def fset(self, value): 93 | if value not in self.SYMMETRIES: 94 | raise ValueError('Type {!r} must be in {!r}.'.format(value, self.SYMMETRIES)) 95 | self._symmetry = value 96 | def fdel(self): 97 | del self._symmetry 98 | return locals() 99 | symmetry = property(**symmetry()) 100 | 101 | def __repr__(self): 102 | s = [ 103 | ('name', self.name), 104 | ('x', self.x), 105 | ('y', self.y), 106 | ('z', self.z), 107 | ('symmetry', self.symmetry), 108 | ('fx_free', self.fx_free), 109 | ('fy_free', self.fy_free), 110 | ('fz_free', self.fz_free), 111 | ('mx_free', self.mx_free), 112 | ('my_free', self.my_free), 113 | ('mz_free', self.mz_free), 114 | ] 115 | 116 | s = ', '.join('{}={!r}'.format(x, y) for x, y in s) 117 | return '{}({})'.format(type(self).__name__, s) 118 | 119 | def __str__(self): 120 | return self.name 121 | 122 | def copy(self): 123 | """Returns a copy of the node.""" 124 | return copy.copy(self) 125 | 126 | def f_fixed(self): 127 | """Sets the node force reactions to fixed.""" 128 | self.fx_free = self.fy_free = self.fz_free = False 129 | return self 130 | 131 | def m_fixed(self): 132 | """Sets the node moment reactions to fixed.""" 133 | self.mx_free = self.my_free = self.mz_free = False 134 | return self 135 | 136 | def fixed(self): 137 | """Sets the node force and moment reactions to fixed.""" 138 | return self.f_fixed().m_fixed() 139 | 140 | def fixities(self): 141 | """Returns the force and moment fixities for the node.""" 142 | return [self.fx_free, self.fy_free, self.fz_free, 143 | self.mx_free, self.my_free, self.mz_free] 144 | 145 | def sym_nodes(self): 146 | """Returns the symmetric nodes for the node.""" 147 | def primary(): 148 | n = self.copy() 149 | n.name = '{}_p'.format(self.name) 150 | return n 151 | 152 | def x_sym(): 153 | n = self.copy() 154 | n.name = '{}_x'.format(self.name) 155 | n[1] *= -1 156 | return n 157 | 158 | def y_sym(): 159 | n = self.copy() 160 | n.name = '{}_y'.format(self.name) 161 | n[0] *= -1 162 | return n 163 | 164 | def xy_sym(): 165 | n = self.copy() 166 | n.name = '{}_xy'.format(self.name) 167 | n[:2] *= -1 168 | return n 169 | 170 | if self.symmetry is None: 171 | return primary(), 172 | 173 | elif self.symmetry == 'x': 174 | return primary(), x_sym() 175 | 176 | elif self.symmetry == 'y': 177 | return primary(), y_sym() 178 | 179 | elif self.symmetry == 'xy': 180 | return primary(), x_sym(), y_sym(), xy_sym() 181 | -------------------------------------------------------------------------------- /civpy/structures/node_load.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import weakref 6 | import numpy as np 7 | 8 | __all__ = ['NodeLoad'] 9 | 10 | 11 | class NodeLoad(np.ndarray): 12 | """ 13 | A class representing a load applied to a node. 14 | 15 | Parameters 16 | ---------- 17 | node : str 18 | The name of the node to which the load will be applied. 19 | fx, fy, fz : float 20 | The applied global node forces. 21 | mx, my, mz : float 22 | The applied global moments. 23 | dx, dy, dz : float 24 | The applied node deflections. 25 | rx, ry, rz : float 26 | The applied node rotations. 27 | """ 28 | def __new__(cls, node, fx=0, fy=0, fz=0, mx=0, my=0, mz=0, 29 | dx=0, dy=0, dz=0, rx=0, ry=0, rz=0): 30 | obj = np.array([fx, fy, fz, mx, my, mz, 31 | dx, dy, dz, rx, ry, rz], dtype='float').view(cls) 32 | obj.node = node 33 | return obj 34 | 35 | def __array_finalize__(self, obj): 36 | if obj is None: return 37 | self.node = getattr(obj, 'node', '') 38 | self.node_ref = None 39 | 40 | def node(): 41 | def fget(self): 42 | return self._node 43 | def fset(self, value): 44 | if not isinstance(value, str): 45 | value = str(value) 46 | self._node = value 47 | def fdel(self): 48 | del self._node 49 | return locals() 50 | node = property(**node()) 51 | 52 | def node_ref(): 53 | def fget(self): 54 | value = self._node_ref 55 | if value is None: 56 | return value 57 | return value() 58 | def fset(self, value): 59 | if value is not None: 60 | value = weakref.ref(value) 61 | self._node_ref = value 62 | def fdel(self): 63 | del self._node_ref 64 | return locals() 65 | node_ref = property(**node_ref()) 66 | 67 | def fx(): 68 | def fget(self): 69 | return self[0] 70 | def fset(self, value): 71 | self[0] = value 72 | return locals() 73 | fx = property(**fx()) 74 | 75 | def fy(): 76 | def fget(self): 77 | return self[1] 78 | def fset(self, value): 79 | self[1] = value 80 | return locals() 81 | fy = property(**fy()) 82 | 83 | def fz(): 84 | def fget(self): 85 | return self[2] 86 | def fset(self, value): 87 | self[2] = value 88 | return locals() 89 | fz = property(**fz()) 90 | 91 | def mx(): 92 | def fget(self): 93 | return self[3] 94 | def fset(self, value): 95 | self[3] = value 96 | return locals() 97 | mx = property(**mx()) 98 | 99 | def my(): 100 | def fget(self): 101 | return self[4] 102 | def fset(self, value): 103 | self[4] = value 104 | return locals() 105 | my = property(**my()) 106 | 107 | def mz(): 108 | def fget(self): 109 | return self[5] 110 | def fset(self, value): 111 | self[5] = value 112 | return locals() 113 | mz = property(**mz()) 114 | 115 | def dx(): 116 | def fget(self): 117 | return self[6] 118 | def fset(self, value): 119 | self[6] = value 120 | def fdel(self): 121 | del self._dx 122 | return locals() 123 | dx = property(**dx()) 124 | 125 | def dy(): 126 | def fget(self): 127 | return self[7] 128 | def fset(self, value): 129 | self[7] = value 130 | def fdel(self): 131 | del self._dy 132 | return locals() 133 | dy = property(**dy()) 134 | 135 | def dz(): 136 | def fget(self): 137 | return self[8] 138 | def fset(self, value): 139 | self[8] = value 140 | def fdel(self): 141 | del self._dz 142 | return locals() 143 | dz = property(**dz()) 144 | 145 | def rx(): 146 | def fget(self): 147 | return self[9] 148 | def fset(self, value): 149 | self[9] = value 150 | def fdel(self): 151 | del self._rx 152 | return locals() 153 | rx = property(**rx()) 154 | 155 | def ry(): 156 | def fget(self): 157 | return self[10] 158 | def fset(self, value): 159 | self[10] = value 160 | def fdel(self): 161 | del self._ry 162 | return locals() 163 | ry = property(**ry()) 164 | 165 | def rz(): 166 | def fget(self): 167 | return self[11] 168 | def fset(self, value): 169 | self[11] = value 170 | def fdel(self): 171 | del self._rz 172 | return locals() 173 | rz = property(**rz()) 174 | 175 | def __repr__(self): 176 | s = [ 177 | 'node={!r}'.format(self.node), 178 | 'forces={!r}'.format((self.fx, self.fy, self.fz)), 179 | 'moments={!r}'.format((self.mx, self.my, self.mz)), 180 | 'defl={!r}'.format((self.dx, self.dy, self.dz)), 181 | 'rot={!r}'.format((self.rx, self.ry, self.rz)) 182 | ] 183 | 184 | return '{}({})'.format(type(self).__name__, ', '.join(s)) 185 | 186 | def forces(self): 187 | """Returns the applied force and moment matrix.""" 188 | return self[:6] 189 | 190 | def deflections(self): 191 | """Returns the applied deflection and rotation matrix.""" 192 | return self[6:] 193 | 194 | def get_node(self): 195 | """Gets the referenced node.""" 196 | if self.node_ref is None: 197 | raise ValueError('Node has not been set.') 198 | return self.node_ref 199 | 200 | def set_node(self, ndict): 201 | """ 202 | Sets the node reference. 203 | 204 | Parameters 205 | ---------- 206 | ndict : dict 207 | A dictionary mapping node names to node objects. 208 | """ 209 | self.node_ref = ndict[self.node] 210 | -------------------------------------------------------------------------------- /civpy/structures/node_load_test.py: -------------------------------------------------------------------------------- 1 | from .node_load import * 2 | 3 | 4 | def test_repr(): 5 | n = NodeLoad('dummy') 6 | repr(n) 7 | -------------------------------------------------------------------------------- /civpy/structures/node_test.py: -------------------------------------------------------------------------------- 1 | from .node import * 2 | 3 | 4 | def test_repr(): 5 | n = Node('1', 1, 2, 3) 6 | repr(Node) 7 | 8 | 9 | def test_str(): 10 | n = Node('1', 1, 2, 3) 11 | assert str(n) == '1' 12 | 13 | 14 | def test_copy(): 15 | n = Node('1', 1, 2, 3) 16 | m = n.copy() 17 | 18 | assert n is not m 19 | assert repr(n) == repr(m) 20 | 21 | 22 | def test_fixities(): 23 | n = Node('1', 1, 2, 3) 24 | a = tuple(n.fixities()) 25 | b = tuple([True] * 6) 26 | 27 | assert a == b 28 | 29 | 30 | def test_sym_nodes(): 31 | # No symmetry 32 | n = Node('1', 1, 2, 3, symmetry=None) 33 | p, = n.sym_nodes() 34 | 35 | assert (p.name, p.x, p.y, p.z) == ('1_p', 1, 2, 3) 36 | 37 | # X symmetry 38 | n = Node('1', 1, 2, 3, symmetry='x') 39 | p, x = n.sym_nodes() 40 | 41 | assert (p.name, p.x, p.y, p.z) == ('1_p', 1, 2, 3) 42 | assert (x.name, x.x, x.y, x.z) == ('1_x', 1, -2, 3) 43 | 44 | # Y symmetry 45 | n = Node('1', 1, 2, 3, symmetry='y') 46 | p, y = n.sym_nodes() 47 | 48 | assert (p.name, p.x, p.y, p.z) == ('1_p', 1, 2, 3) 49 | assert (y.name, y.x, y.y, y.z) == ('1_y', -1, 2, 3) 50 | 51 | # XY symmetry 52 | n = Node('1', 1, 2, 3, symmetry='xy') 53 | p, x, y, xy = n.sym_nodes() 54 | 55 | assert (p.name, p.x, p.y, p.z) == ('1_p', 1, 2, 3) 56 | assert (x.name, x.x, x.y, x.z) == ('1_x', 1, -2, 3) 57 | assert (y.name, y.x, y.y, y.z) == ('1_y', -1, 2, 3) 58 | assert (xy.name, xy.x, xy.y, xy.z) == ('1_xy', -1, -2, 3) 59 | -------------------------------------------------------------------------------- /civpy/structures/structure.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import attr 6 | import inspect 7 | import numpy as np 8 | import pandas as pd 9 | from functools import wraps 10 | import matplotlib.pyplot as plt 11 | from .element import clear_element_cache 12 | from .element_load import clear_element_load_cache 13 | from ..math import rotation_matrix3 14 | 15 | __all__ = ['Structure'] 16 | 17 | 18 | def build(func): 19 | """ 20 | A decorator that ensures the :class:`.Structure` is built prior to 21 | executing the wrapped method. 22 | """ 23 | def index(lst, s): 24 | for i, x in enumerate(lst): 25 | if x == s: 26 | return i 27 | return -1 28 | 29 | @wraps(func) 30 | def wrapper(obj, *args, **kwargs): 31 | # If already built, perform the operation 32 | if obj._build: 33 | return func(obj, *args, **kwargs) 34 | 35 | # If not built, build it, perform the operation, and destroy the build 36 | if n == -1: 37 | lc = [] 38 | elif n-1 < len(args): 39 | lc = args[n-1] 40 | else: 41 | lc = kwargs['lc'] 42 | 43 | if not isinstance(lc, (list, tuple)): 44 | lc = [lc] 45 | 46 | obj._create_build(lc) 47 | result = func(obj, *args, **kwargs) 48 | obj._build.clear() 49 | clear_element_cache() 50 | clear_element_load_cache() 51 | 52 | return result 53 | 54 | # Determine if lc argument exists 55 | n = index(inspect.getfullargspec(func).args, 'lc') 56 | 57 | return wrapper 58 | 59 | 60 | @attr.s(hash=False) 61 | class Structure(object): 62 | """ 63 | A class representing a structure. 64 | 65 | Parameters 66 | ---------- 67 | name : str 68 | The name of the structure. 69 | nodes : list 70 | A list of :class:`.Node`. 71 | elements : list 72 | A list of :class:`.Element`. 73 | symmetry : bool 74 | If True, symmetry will be applied to the structure. 75 | 76 | Examples 77 | -------- 78 | The following example creates an a structure and performs linear analysis 79 | for a load case. 80 | 81 | .. plot:: ../examples/structures/structure_ex1.py 82 | :include-source: 83 | """ 84 | name = attr.ib() 85 | nodes = attr.ib() 86 | elements = attr.ib() 87 | symmetry = attr.ib(default=False) 88 | _build = attr.ib(default={}, init=False, repr=False) 89 | 90 | def _create_build(self, load_cases=[]): 91 | """ 92 | Builds the structure and places all components in the object 93 | model dictionary. 94 | 95 | Parameters 96 | ---------- 97 | load_cases : list 98 | A list of :class:`.LoadCase`. 99 | """ 100 | if not self.symmetry: 101 | nodes = self.nodes 102 | elements = self.elements 103 | else: 104 | # Make symmetric components 105 | nodes = [] 106 | for n in self.nodes: 107 | nodes += n.sym_nodes() 108 | 109 | elements = [] 110 | for e in self.elements: 111 | elements += e.sym_elements() 112 | 113 | ndict = {n.name: n for n in nodes} 114 | edict = {e.name: e for e in elements} 115 | 116 | # Set nodes to elements 117 | for e in elements: 118 | e.set_nodes(ndict) 119 | 120 | # Set nodes and elements to loads 121 | for lc in load_cases: 122 | lc.set_nodes(ndict) 123 | lc.set_elements(edict) 124 | 125 | ndict = {n.name: 6*i for i, n in enumerate(nodes)} 126 | edict = {e.name: i for i, e in enumerate(elements)} 127 | 128 | self._build = { 129 | 'nodes': nodes, 130 | 'elements': elements, 131 | 'ndict': ndict, 132 | 'edict': edict, 133 | 'load_cases': load_cases 134 | } 135 | 136 | @build 137 | def plot_3d(self, ax=None, symbols={}): 138 | """ 139 | Plots the structure in 3D. 140 | 141 | Parameters 142 | ---------- 143 | ax 144 | The axes to which the plot will be added. If None, a new figure 145 | and axes will be created. 146 | symbols : dict 147 | The plot symbols with any of the following keys: 148 | 149 | * 'nodes': The node point symbols, default is 'r.' 150 | * 'elements': The element lines, default is 'b--'. 151 | """ 152 | # Build the structure 153 | x = np.array(self._build['nodes']) 154 | 155 | # Create figure is one not provided 156 | if ax is None: 157 | mx = x.max(axis=0) 158 | c = 0.5 * (mx + x.min(axis=0)) 159 | rng = 1.1 * np.max(mx - c) 160 | xlim, ylim, zlim = np.column_stack([c - rng, c + rng]) 161 | 162 | fig = plt.figure() 163 | ax = fig.add_subplot(111, 164 | projection='3d', 165 | xlim=xlim, 166 | ylim=ylim, 167 | zlim=zlim, 168 | xlabel='X', 169 | ylabel='Y', 170 | zlabel='Z', 171 | aspect='equal' 172 | ) 173 | 174 | # Symbols 175 | sym = dict( 176 | elements='b--', 177 | nodes='r.', 178 | ntext='k', 179 | etext='r' 180 | ) 181 | sym.update(symbols) 182 | 183 | # Plot elements 184 | if sym['elements'] is not None: 185 | for e in self._build['elements']: 186 | e = np.array(e.get_nodes()) 187 | ax.plot(e[:,0], e[:,1], e[:,2], sym['elements']) 188 | 189 | # Plot element text 190 | if sym['etext'] is not None: 191 | for e in self._build['elements']: 192 | p, q = e.get_nodes() 193 | p = (q - p) / 3 + p 194 | ax.text(p[0], p[1], p[2], e.name, ha='center', 195 | va='center', color=sym['etext']) 196 | 197 | # Plot nodes 198 | if sym['nodes'] is not None: 199 | ax.plot(x[:,0], x[:,1], x[:,2], sym['nodes']) 200 | 201 | # Plot node text 202 | if sym['ntext'] is not None: 203 | for n in self._build['nodes']: 204 | ax.text(n[0], n[1], n[2], n.name, color=sym['ntext']) 205 | 206 | return ax 207 | 208 | @build 209 | def plot_2d(self, ax=None, angle_x=0, angle_y=0, 210 | angle_z=0, symbols={}): 211 | """ 212 | Plots the 2D projection of the structure. 213 | 214 | Parameters 215 | ---------- 216 | ax 217 | The axes to which the plot will be added. If None, a new figure 218 | and axes will be created. 219 | angle_x, angle_y, angle_z : float 220 | The rotation angles about the x, y, and z axes. 221 | symbols : dict 222 | The plot symbols with any of the following keys: 223 | 224 | * 'nodes': The node point symbols, default is 'r.' 225 | * 'elements': The element lines, default is 'b--'. 226 | """ 227 | # Build the structure 228 | r = rotation_matrix3(angle_x, angle_y, angle_z).T 229 | x = np.array(self._build['nodes']).dot(r) 230 | 231 | # Create figure is one not provided 232 | if ax is None: 233 | mx = x.max(axis=0) 234 | c = 0.5 * (mx + x.min(axis=0)) 235 | rng = 1.1 * np.max(mx - c) 236 | xlim, ylim, _ = np.column_stack([c - rng, c + rng]) 237 | 238 | fig = plt.figure() 239 | ax = fig.add_subplot(111, 240 | xlim=xlim, 241 | ylim=ylim, 242 | xlabel="X'", 243 | ylabel="Y'", 244 | aspect='equal' 245 | ) 246 | 247 | # Symbols 248 | sym = dict( 249 | elements='b--', 250 | nodes='r.', 251 | ntext='k', 252 | etext='r' 253 | ) 254 | sym.update(symbols) 255 | 256 | # Plot elements 257 | if sym['elements'] is not None: 258 | for e in self._build['elements']: 259 | e = np.array(e.get_nodes()).dot(r) 260 | ax.plot(e[:,0], e[:,1], sym['elements']) 261 | 262 | # Plot element text 263 | if sym['etext'] is not None: 264 | for e in self._build['elements']: 265 | p = np.array(e.get_nodes()).dot(r) 266 | p = (p[1] - p[0]) / 3 + p[0] 267 | ax.text(p[0], p[1], e.name, ha='center', 268 | va='center', color=sym['etext']) 269 | 270 | # Plot nodes 271 | if sym['nodes'] is not None: 272 | ax.plot(x[:,0], x[:,1], sym['nodes']) 273 | 274 | # Plot node text 275 | if sym['ntext'] is not None: 276 | for n in self._build['nodes']: 277 | p = n.dot(r) 278 | ax.text(p[0], p[1], n.name, color=sym['ntext']) 279 | 280 | return ax 281 | 282 | @build 283 | def global_stiffness(self, defl=None): 284 | """ 285 | Returns the global stiffness matrix for the structure. 286 | 287 | Parameters 288 | ---------- 289 | defl : array 290 | The deflection matrix. If None, all deflections will be 291 | assumed to be zero. 292 | """ 293 | n = len(self._build['nodes']) 294 | k = np.zeros((6*n, 6*n), dtype='float') 295 | ndict = self._build['ndict'] 296 | 297 | if defl is None: 298 | defl = np.zeros(6*n) 299 | 300 | for e in self.elements: 301 | i, j = ndict[e.inode], ndict[e.jnode] 302 | di, dj = defl[i:i+3], defl[j:j+3] 303 | 304 | ke = e.global_stiffness(di, dj) 305 | k[i:i+6,i:i+6] += ke[:6,:6] 306 | k[i:i+6,j:j+6] += ke[:6,6:12] 307 | k[j:j+6,i:i+6] += ke[6:12,:6] 308 | k[j:j+6,j:j+6] += ke[6:12,6:12] 309 | 310 | return k 311 | 312 | @build 313 | def global_node_loads(self, lc): 314 | """ 315 | Returns the global node load matrix for the input load case. 316 | 317 | Parameters 318 | ---------- 319 | lc : :class:`.LoadCase` 320 | The applied load case. 321 | """ 322 | n = len(self._build['nodes']) 323 | q = np.zeros(6*n, dtype='float') 324 | ndict = self._build['ndict'] 325 | 326 | for n in lc.node_loads: 327 | i = ndict[n.node] 328 | q[i:i+6] += n.forces() 329 | 330 | return q 331 | 332 | @build 333 | def local_elem_loads(self, lc, defl=None): 334 | """ 335 | Returns the local element loads for the input load case. 336 | 337 | Parameters 338 | ---------- 339 | lc : :class:`.LoadCase` 340 | The applied load case. 341 | defl : array 342 | The global node deflections. 343 | """ 344 | n = len(self._build['nodes']) 345 | m = len(self._build['elements']) 346 | q = np.zeros((m, 12), dtype='float') 347 | ndict = self._build['ndict'] 348 | edict = self._build['edict'] 349 | 350 | if defl is None: 351 | defl = np.zeros(6*n) 352 | 353 | for e in lc.elem_loads: 354 | ref = e.get_element() 355 | i, j, k = ndict[ref.inode], ndict[ref.jnode], edict[ref.name] 356 | di, dj = defl[i:i+3], defl[j:j+3] 357 | q[k] += e.local_reactions(di, dj) 358 | 359 | return q 360 | 361 | @build 362 | def global_elem_loads(self, lc, defl=None): 363 | """ 364 | Returns the global node load matrix for the input load case. 365 | 366 | Parameters 367 | ---------- 368 | lc : :class:`.LoadCase` 369 | The applied load case. 370 | defl : array 371 | The global node deflections. 372 | """ 373 | n = len(self._build['nodes']) 374 | q = np.zeros(6*n, dtype='float') 375 | ndict = self._build['ndict'] 376 | 377 | if defl is None: 378 | defl = np.zeros(6*n) 379 | 380 | for e in lc.elem_loads: 381 | ref = e.get_element() 382 | i, j = ndict[ref.inode], ndict[ref.jnode] 383 | di, dj = defl[i:i+3], defl[j:j+3] 384 | 385 | f = e.global_reactions(di, dj) 386 | q[i:i+6] += f[:6] 387 | q[j:j+6] += f[6:12] 388 | 389 | return q 390 | 391 | @build 392 | def global_defl(self, lc): 393 | """ 394 | Returns the global applied deflection matrix for the input load case. 395 | 396 | Parameters 397 | ---------- 398 | lc : :class:`.LoadCase` 399 | The applied load case. 400 | """ 401 | n = len(self._build['nodes']) 402 | d = np.zeros(6*n, dtype='float') 403 | ndict = self._build['ndict'] 404 | 405 | for n in lc.node_loads: 406 | i = ndict[n.node] 407 | d[i:i+6] += n.deflections() 408 | 409 | return d 410 | 411 | def _create_summary(self, r): 412 | """ 413 | Creates dataframe summaries for the structural analysis results. 414 | 415 | Parameters 416 | ---------- 417 | r : dict 418 | A dictionary of result arrays. 419 | """ 420 | n = len(self._build['nodes']) 421 | m = len(self._build['elements']) 422 | lc = self._build['load_cases'] 423 | u = [x.fixities() for x in self.nodes] * len(lc) 424 | u = np.array(u, dtype='bool') 425 | 426 | # Global load data frame 427 | df1 = pd.DataFrame() 428 | df1['load_case'] = np.array([[l.name] * n for l in lc]).ravel() 429 | df1['node'] = [x.name for x in self._build['nodes']] * len(lc) 430 | 431 | # Process global forces 432 | x = np.array(r.pop('glob_force')).reshape(-1, 6) 433 | x[np.abs(x) < 1e-8] = 0 434 | df1['force_x'] = x[:,0] 435 | df1['force_y'] = x[:,1] 436 | df1['force_z'] = x[:,2] 437 | df1['moment_x'] = x[:,3] 438 | df1['moment_y'] = x[:,4] 439 | df1['moment_z'] = x[:,5] 440 | 441 | # Process global deflections 442 | x = np.array(r.pop('glob_defl')).reshape(-1, 6) 443 | x[np.abs(x) < 1e-8] = 0 444 | df1['defl_x'] = x[:,0] 445 | df1['defl_y'] = x[:,1] 446 | df1['defl_z'] = x[:,2] 447 | df1['rot_x'] = x[:,3] 448 | df1['rot_y'] = x[:,4] 449 | df1['rot_z'] = x[:,5] 450 | 451 | # Global reaction data frame 452 | df2 = df1.copy() 453 | del df2['defl_x'], df2['defl_y'], df2['defl_z'] 454 | del df2['rot_x'], df2['rot_y'], df2['rot_z'] 455 | 456 | df2.loc[u[:,0], 'force_x'] = np.nan 457 | df2.loc[u[:,1], 'force_y'] = np.nan 458 | df2.loc[u[:,2], 'force_z'] = np.nan 459 | df2.loc[u[:,3], 'moment_x'] = np.nan 460 | df2.loc[u[:,4], 'moment_y'] = np.nan 461 | df2.loc[u[:,5], 'moment_z'] = np.nan 462 | 463 | df2 = df2[~u.all(axis=1)].copy() 464 | df2 = df2.reset_index(drop=True) 465 | 466 | # Local reaction data frame 467 | df3 = pd.DataFrame() 468 | df3['load_case'] = np.array([[l.name] * m for l in lc]).ravel() 469 | df3['element'] = [x.name for x in self._build['elements']] * len(lc) 470 | 471 | # Process local forces 472 | x = np.array(r.pop('loc_force')).reshape(-1, 12) 473 | x[np.abs(x) < 1e-8] = 0 474 | 475 | df3['i_axial'] = x[:,0] 476 | df3['i_shear_x'] = x[:,1] 477 | df3['i_shear_y'] = x[:,2] 478 | df3['i_torsion'] = x[:,3] 479 | df3['i_moment_x'] = x[:,4] 480 | df3['i_moment_y'] = x[:,5] 481 | 482 | df3['j_axial'] = x[:,6] 483 | df3['j_shear_x'] = x[:,7] 484 | df3['j_shear_y'] = x[:,8] 485 | df3['j_torsion'] = x[:,9] 486 | df3['j_moment_x'] = x[:,10] 487 | df3['j_moment_y'] = x[:,11] 488 | 489 | # Process local deflections 490 | x = np.array(r.pop('loc_defl')).reshape(-1, 12) 491 | x[np.abs(x) < 1e-8] = 0 492 | 493 | df3['i_defl_ax'] = x[:,0] 494 | df3['i_defl_x'] = x[:,1] 495 | df3['i_defl_y'] = x[:,2] 496 | df3['i_twist'] = x[:,3] 497 | df3['i_rot_x'] = x[:,4] 498 | df3['i_rot_y'] = x[:,5] 499 | 500 | df3['j_defl_ax'] = x[:,6] 501 | df3['j_defl_x'] = x[:,7] 502 | df3['j_defl_y'] = x[:,8] 503 | df3['j_twist'] = x[:,9] 504 | df3['j_rot_x'] = x[:,10] 505 | df3['j_rot_y'] = x[:,11] 506 | 507 | return dict(glob=df1, react=df2, loc=df3) 508 | 509 | @build 510 | def linear_analysis(self, lc): 511 | """ 512 | Performs linear analysis on the structure. 513 | 514 | Parameters 515 | ---------- 516 | lc : :class:`.LoadCase` or list 517 | A load case or list of load cases to perform analysis for. 518 | """ 519 | n = len(self._build['nodes']) 520 | k = self.global_stiffness() 521 | ndict = self._build['ndict'] 522 | 523 | # Result dictionary 524 | r = dict(glob_force=[], glob_defl=[], loc_force=[], loc_defl=[]) 525 | 526 | # Determine free and nonzero matrix rows and columns 527 | u = np.array([x.fixities() for x in self.nodes], dtype='bool').ravel() 528 | 529 | if not u.any(): 530 | raise ValueError('No node fixities found.') 531 | 532 | u &= k.any(axis=1) 533 | v = ~u 534 | 535 | # Calculate inverse and create unknown-known stiffness partition 536 | ki = np.linalg.inv(k[u][:,u]) 537 | kuv = k[u][:,v] 538 | 539 | for l in self._build['load_cases']: 540 | # Find unknown deflections and global forces 541 | d = self.global_defl(l) 542 | q = self.global_node_loads(l) 543 | qe = self.global_elem_loads(l) 544 | q -= qe 545 | d[u] = ki.dot(q[u] - kuv.dot(d[v])) 546 | q = k.dot(d) + qe 547 | 548 | # Add to results dictionary 549 | r['glob_force'].append(q) 550 | r['glob_defl'].append(d) 551 | 552 | # Find local forces 553 | q = self.local_elem_loads(l) 554 | 555 | for m, e in enumerate(self._build['elements']): 556 | i, j = ndict[e.inode], ndict[e.jnode] 557 | dl = np.array([d[i:i+6], d[j:j+6]]).ravel() 558 | dl = e.transformation_matrix().dot(dl) 559 | q[m] += e.local_stiffness().dot(dl) 560 | r['loc_defl'].append(dl) 561 | 562 | r['loc_force'].append(q) 563 | 564 | # Destory obsolete objects 565 | del k, ki, kuv, u, v, d, q 566 | 567 | return self._create_summary(r) 568 | -------------------------------------------------------------------------------- /civpy/structures/structure_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from . import * 4 | 5 | 6 | def Structure1(): 7 | s = CrossSection('dummy', 8 | area=32.9, 9 | inertia_x=236, 10 | inertia_y=716, 11 | inertia_j=15.1 12 | ) 13 | 14 | m = Material('dummy', 15 | elasticity=29000, 16 | rigidity=11500 17 | ) 18 | 19 | g = ElementGroup('dummy', s, m) 20 | 21 | nodes = [ 22 | Node('1', 0, 0, 0), 23 | Node('2', -240, 0, 0).fixed(), 24 | Node('3', 0, -240, 0).fixed(), 25 | Node('4', 0, 0, -240).fixed(), 26 | ] 27 | 28 | elements =[ 29 | Element('1', '2', '1', g), 30 | Element('2', '3', '1', g, roll=np.deg2rad(-90)), 31 | Element('3', '4', '1', g, roll=np.deg2rad(-30)), 32 | ] 33 | 34 | return Structure('dummy', nodes, elements) 35 | 36 | 37 | def test_rotation_matrix(): 38 | struct = Structure1() 39 | struct._create_build() 40 | e1, e2, e3 = struct._build['elements'] 41 | 42 | a = e1.rotation_matrix().ravel() 43 | b = np.identity(3).ravel() 44 | 45 | assert pytest.approx(a) == b 46 | 47 | a = e2.rotation_matrix().ravel() 48 | b = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]]).ravel() 49 | 50 | assert pytest.approx(a) == b 51 | 52 | a = e3.rotation_matrix().ravel() 53 | b = np.array([[0, 0, 1], [-0.5, 0.86603, 0], [-0.86603, -0.5, 0]]).ravel() 54 | 55 | assert pytest.approx(a, 0.01) == b 56 | 57 | 58 | def test_transformation_matrix(): 59 | struct = Structure1() 60 | struct._create_build() 61 | e1, e2, e3 = struct._build['elements'] 62 | 63 | a = e1.transformation_matrix().ravel() 64 | b = np.identity(12).ravel() 65 | 66 | assert pytest.approx(a) == b 67 | 68 | a = e2.transformation_matrix().ravel() 69 | b = np.zeros((12, 12), dtype='float') 70 | r = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]]) 71 | b[:3,:3] = b[3:6,3:6] = b[6:9,6:9] = b[9:12,9:12] = r 72 | b = b.ravel() 73 | 74 | assert pytest.approx(a) == b 75 | 76 | a = e3.transformation_matrix().ravel() 77 | b = np.zeros((12, 12), dtype='float') 78 | r = np.array([[0, 0, 1], [-0.5, 0.86603, 0], [-0.86603, -0.5, 0]]) 79 | b[:3,:3] = b[3:6,3:6] = b[6:9,6:9] = b[9:12,9:12] = r 80 | b = b.ravel() 81 | 82 | assert pytest.approx(a, 0.1) == b 83 | 84 | 85 | def test_length(): 86 | struct = Structure1() 87 | struct._create_build() 88 | e1, e2, e3 = struct._build['elements'] 89 | 90 | a = e1.length() 91 | assert pytest.approx(a) == 240 92 | 93 | a = e2.length() 94 | assert pytest.approx(a) == 240 95 | 96 | a = e3.length() 97 | assert pytest.approx(a) == 240 98 | 99 | 100 | def test_local_stiffness(): 101 | struct = Structure1() 102 | struct._create_build() 103 | e1, _, _ = struct._build['elements'] 104 | 105 | a = e1.local_stiffness().ravel() 106 | b = np.array([ 107 | [3975.4, 0, 0, 0, 0, 0, -3975.4, 0, 0, 0, 0, 0], 108 | [0, 18.024, 0, 0, 0, 2162.9, 0, -18.024, 0, 0, 0, 2162.9], 109 | [0, 0, 5.941, 0, -712.92, 0, 0, 0, -5.941, 0, -712.92, 0], 110 | [0, 0, 0, 723.54, 0, 0, 0, 0, 0, -723.54, 0, 0], 111 | [0, 0, -712.92, 0, 114067, 0, 0, 0, 712.92, 0, 57033, 0], 112 | [0, 2162.9, 0, 0, 0, 346067, 0, -2162.9, 0, 0, 0, 173033], 113 | [-3975.4, 0, 0, 0, 0, 0, 3975.4, 0, 0, 0, 0, 0], 114 | [0, -18.024, 0, 0, 0, -2162.9, 0, 18.024, 0, 0, 0, -2162.9], 115 | [0, 0, -5.941, 0, 712.92, 0, 0, 0, 5.941, 0, 712.92, 0], 116 | [0, 0, 0, -723.54, 0, 0, 0, 0, 0, 723.54, 0, 0], 117 | [0, 0, -712.92, 0, 57033, 0, 0, 0, 712.92, 0, 114067, 0], 118 | [0, 2162.9, 0, 0, 0, 173033, 0, -2162.9, 0, 0, 0, 346067] 119 | ]).ravel() 120 | 121 | assert pytest.approx(a, 0.01) == b 122 | 123 | 124 | def test_global_stiffness(): 125 | struct = Structure1() 126 | struct._create_build() 127 | e1, e2, e3 = struct._build['elements'] 128 | 129 | a = e1.global_stiffness().ravel() 130 | b = np.array([ 131 | [3975.4, 0, 0, 0, 0, 0, -3975.4, 0, 0, 0, 0, 0], 132 | [0, 18.024, 0, 0, 0, 2162.9, 0, -18.024, 0, 0, 0, 2162.9], 133 | [0, 0, 5.941, 0, -712.92, 0, 0, 0, -5.941, 0, -712.92, 0], 134 | [0, 0, 0, 723.54, 0, 0, 0, 0, 0, -723.54, 0, 0], 135 | [0, 0, -712.92, 0, 114067, 0, 0, 0, 712.92, 0, 57033, 0], 136 | [0, 2162.9, 0, 0, 0, 346067, 0, -2162.9, 0, 0, 0, 173033], 137 | [-3975.4, 0, 0, 0, 0, 0, 3975.4, 0, 0, 0, 0, 0], 138 | [0, -18.024, 0, 0, 0, -2162.9, 0, 18.024, 0, 0, 0, -2162.9], 139 | [0, 0, -5.941, 0, 712.92, 0, 0, 0, 5.941, 0, 712.92, 0], 140 | [0, 0, 0, -723.54, 0, 0, 0, 0, 0, 723.54, 0, 0], 141 | [0, 0, -712.92, 0, 57033, 0, 0, 0, 712.92, 0, 114067, 0], 142 | [0, 2162.9, 0, 0, 0, 173033, 0, -2162.9, 0, 0, 0, 346067] 143 | ]).ravel() 144 | 145 | assert pytest.approx(a, 0.01) == b 146 | 147 | a = e2.global_stiffness().ravel() 148 | b = np.array([ 149 | [5.941, 0, 0, 0, 0, -712.92, -5.941, 0, 0, 0, 0, -712.92], 150 | [0, 3975.4, 0, 0, 0, 0, 0, -3975.4, 0, 0, 0, 0], 151 | [0, 0, 18.024, 2162.9, 0, 0, 0, 0, -18.024, 2162.9, 0, 0], 152 | [0, 0, 2162.9, 346067, 0, 0, 0, 0, -2162.9, 173033, 0, 0], 153 | [0, 0, 0, 0, 723.54, 0, 0, 0, 0, 0, -723.54, 0], 154 | [-5.941, 0, 0, 0, 0, 712.92, 5.941, 0, 0, 0, 0, 712.92], 155 | [0, -3975.4, 0, 0, 0, 0, 0, 3975.4, 0, 0, 0, 0], 156 | [0, 0, -18.024, -2162.9, 0, 0, 0, 0, 18.024, -2162.9, 0, 0], 157 | [0, 0, 2162.9, 173033, 0, 0, 0, 0, -2162.9, 346067, 0, 0], 158 | [0, 0, 0, 0, -723.54, 0, 0, 0, 0, 0, 723.54, 0], 159 | [-712.92, 0, 0, 0, 0, 57033, 712.92, 0, 0, 0, 0, 114067] 160 | ]).ravel() 161 | 162 | a = e3.global_stiffness().ravel() 163 | b = np.array([ 164 | [8.9618, -5.2322, 0, 627.87, 1075.4, 0, -8.9618, 5.2322, 0, 627.87, 1075.4, 0], 165 | [-5.2322, 15.003, 0, -1800.4, -627.87, 0, 5.2322, -15.003, 0, -1800.4, -627.87, 0], 166 | [0, 0, 3975.4, 0, 0, 0, 0, 0, -3975.4, 0, 0, 0], 167 | [627.87, -1800.4, 0, 288067, 100459, 0, -627.87, 1800.4, 0, 144033, 50229, 0], 168 | [1075.4, -627.87, 0, 100459, 172067, 0, -1075.4, 627.87, 0, 50229, 86033, 0], 169 | [0, 0, 0, 0, 0, 723.54, 0, 0, 0, 0, 0, -723.54], 170 | [-8.9618, 5.2322, 0, -627.87, -1075.4, 0, 8.9618, -5.2322, 0, -627.87, -1075.4, 0], 171 | [5.2322, -15.003, 0, 1800.4, 627.87, 0, -5.2322, 15.003, 0, 1800.4, 627.87, 0], 172 | [0, 0, -3975.4, 0, 0, 0, 0, 0, 3975.4, 0, 0, 0], 173 | [627.87, -1800.4, 0, 144033, 50229, 0, -627.87, 1800.4, 0, 288067, 100459, 0], 174 | [1075.4, -627.87, 0, 50229, 86033, 0, -1075.4, 627.87, 0, 100459, 172067, 0], 175 | [0, 0, 0, 0, 0, -723.54, 0, 0, 0, 0, 0, 723.54] 176 | ]).ravel() 177 | 178 | assert pytest.approx(a, 0.01) == b 179 | 180 | 181 | def test_plot_2d(): 182 | struct = Structure1() 183 | struct.plot_2d() 184 | 185 | 186 | def test_plot_3d(): 187 | struct = Structure1() 188 | struct.plot_3d() 189 | 190 | 191 | def test_linear_analysis(): 192 | struct = Structure1() 193 | 194 | nloads = [NodeLoad('1', mx=-1800, mz=1800)] 195 | eloads = [ElementLoad('1', fy=-0.25)] 196 | lc = LoadCase('1', nloads, eloads) 197 | 198 | r = struct.linear_analysis(lc) 199 | 200 | # Test global loads 201 | df = r['glob'] 202 | 203 | # Loads 204 | s = ['force_x', 'force_y', 'force_z', 'moment_x', 'moment_y', 'moment_z'] 205 | 206 | a = np.array(df[df['node'] == '1'][s].iloc[0]) 207 | b = np.array([0, 0, 0, -1800, 0, 1800]) 208 | assert pytest.approx(a, 0.01) == b 209 | 210 | a = np.array(df[df['node'] == '2'][s].iloc[0]) 211 | b = np.array([5.3757, 44.106, -0.74272, 2.1722, 58.987, 2330.5]) 212 | assert pytest.approx(a, 0.01) == b 213 | 214 | a = np.array(df[df['node'] == '3'][s].iloc[0]) 215 | b = np.array([-4.6249, 11.117, -6.4607, -515.55, -0.76472, 369.67]) 216 | assert pytest.approx(a, 0.01) == b 217 | 218 | a = np.array(df[df['node'] == '4'][s].iloc[0]) 219 | b = np.array([-0.75082, 4.7763, 7.2034, -383.5, -60.166, -4.702]) 220 | assert pytest.approx(a, 0.01) == b 221 | 222 | # Deflections 223 | s = ['defl_x', 'defl_y', 'defl_z', 'rot_x', 'rot_y', 'rot_z'] 224 | 225 | a = np.array(df[df['node'] == '1'][s].iloc[0]) 226 | b = np.array([-1.3522, -2.7965, -1.812, -3.0021, 1.0569, 6.4986]) * 1e-3 227 | assert pytest.approx(a, 0.01) == b 228 | 229 | a = np.array(df[df['node'] == '2'][s].iloc[0]) 230 | b = np.zeros(6) 231 | assert pytest.approx(a, 0.01) == b 232 | 233 | a = np.array(df[df['node'] == '3'][s].iloc[0]) 234 | b = np.zeros(6) 235 | assert pytest.approx(a, 0.01) == b 236 | 237 | a = np.array(df[df['node'] == '4'][s].iloc[0]) 238 | b = np.zeros(6) 239 | assert pytest.approx(a, 0.01) == b 240 | 241 | 242 | # Test reactions 243 | df = r['react'] 244 | s = ['force_x', 'force_y', 'force_z', 'moment_x', 'moment_y', 'moment_z'] 245 | 246 | a = np.array(df[df['node'] == '2'][s].iloc[0]) 247 | b = np.array([5.3757, 44.106, -0.74272, 2.1722, 58.987, 2330.5]) 248 | assert pytest.approx(a, 0.01) == b 249 | 250 | a = np.array(df[df['node'] == '3'][s].iloc[0]) 251 | b = np.array([-4.6249, 11.117, -6.4607, -515.55, -0.76472, 369.67]) 252 | assert pytest.approx(a, 0.01) == b 253 | 254 | a = np.array(df[df['node'] == '4'][s].iloc[0]) 255 | b = np.array([-0.75082, 4.7763, 7.2034, -383.5, -60.166, -4.702]) 256 | assert pytest.approx(a, 0.01) == b 257 | 258 | 259 | # Test local loads 260 | df = r['loc'] 261 | 262 | # Loads 263 | s = ['i_axial', 'i_shear_x', 'i_shear_y', 264 | 'i_torsion', 'i_moment_x', 'i_moment_y', 265 | 'j_axial', 'j_shear_x', 'j_shear_y', 266 | 'j_torsion', 'j_moment_x', 'j_moment_y'] 267 | 268 | a = np.array(df[df['element'] == '1'][s].iloc[0]) 269 | b = np.array([5.3757, 44.106, -0.74272, 2.1722, 58.987, 2330.5, 270 | -5.3757, 15.894, 0.74272, -2.1722, 119.27, 1055]) 271 | assert pytest.approx(a, 0.01) == b 272 | 273 | a = np.array(df[df['element'] == '2'][s].iloc[0]) 274 | b = np.array([11.117, -6.4607, -4.6249, -0.76472, 369.67, -515.55, 275 | -11.117, 6.4607, 4.6249, 0.76472, 740.31, -1035]) 276 | assert pytest.approx(a, 0.01) == b 277 | 278 | a = np.array(df[df['element'] == '3'][s].iloc[0]) 279 | b = np.array([7.2034, 4.5118, -1.7379, -4.702, 139.65, 362.21, 280 | -7.2034, -4.5118, 1.7379, 4.702, 277.46, 720.63]) 281 | assert pytest.approx(a, 0.01) == b 282 | 283 | # Deflections 284 | s = ['i_defl_ax', 'i_defl_x', 'i_defl_y', 285 | 'i_twist', 'i_rot_x', 'i_rot_y', 286 | 'j_defl_ax', 'j_defl_x', 'j_defl_y', 287 | 'j_twist', 'j_rot_x', 'j_rot_y'] 288 | 289 | a = np.array(df[df['element'] == '1'][s].iloc[0]) 290 | b = np.array([0, 0, 0, 0, 0, 0, 291 | -1.3522, -2.7965, -1.812, -3.0021, 1.0569, 6.4986]) * 1e-3 292 | assert pytest.approx(a, 0.01) == b 293 | 294 | a = np.array(df[df['element'] == '2'][s].iloc[0]) 295 | b = np.array([0, 0, 0, 0, 0, 0, 296 | -2.7965, -1.812, -1.3522, 1.0569, 6.4986, -3.0021]) * 1e-3 297 | assert pytest.approx(a, 0.01) == b 298 | 299 | a = np.array(df[df['element'] == '3'][s].iloc[0]) 300 | b = np.array([0, 0, 0, 0, 0, 0, 301 | -1.812, -1.7457, 2.5693, 6.4986, 2.4164, 2.0714]) * 1e-3 302 | assert pytest.approx(a, 0.01) == b 303 | -------------------------------------------------------------------------------- /civpy/survey/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ============================ 3 | Survey (:mod:`civpy.survey`) 4 | ============================ 5 | 6 | Contains functions for modeling alignments and performing spatial queries. 7 | 8 | 9 | Spatial Models 10 | ============== 11 | .. autosummary:: 12 | :toctree: generated/ 13 | 14 | SpatialHash 15 | SurveyPoint 16 | TIN 17 | 18 | .. plot:: ../examples/survey/spatial_hash_ex2.py 19 | 20 | 21 | Alignment 22 | ========= 23 | .. autosummary:: 24 | :toctree: generated/ 25 | 26 | PI 27 | SurveyStake 28 | Alignment 29 | 30 | .. plot:: ../examples/survey/alignment_ex1.py 31 | """ 32 | 33 | from .alignment import * 34 | from .pi import * 35 | from .spatial_hash import * 36 | from .survey_point import * 37 | from .survey_stake import * 38 | from .tin import * 39 | -------------------------------------------------------------------------------- /civpy/survey/alignment.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import attr 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | from .spatial_hash import SpatialHash 9 | 10 | __all__ = ['Alignment'] 11 | 12 | 13 | @attr.s(hash=False) 14 | class Alignment(object): 15 | """ 16 | A class representing a survey alignment. 17 | 18 | Parameters 19 | ---------- 20 | name : str 21 | Name of alignment. 22 | pis : list 23 | A list of :class:`.PI`. 24 | stakes : list 25 | A list of :class:`.SurveyStake`. 26 | grid : float 27 | The grid size used for spatial hash generation. 28 | view_offset : float 29 | The offset beyond which points will be ignored when generating station 30 | coordinates from global coordinates. 31 | view_margin : float 32 | The station margin at the beginning and end of the alignment. Beyond 33 | this threshold, generated station coordinates from global coordinates 34 | will be ignored. 35 | 36 | Examples 37 | -------- 38 | .. plot:: ../examples/survey/alignment_ex1.py 39 | :include-source: 40 | """ 41 | # Global class variables 42 | BISC_TOL = 1e-4 # Bisector station tolerance 43 | 44 | # Properties 45 | name = attr.ib() 46 | pis = attr.ib(default=[]) 47 | stakes = attr.ib(default=[]) 48 | grid = attr.ib(default=10) 49 | view_offset = attr.ib(default=15) 50 | view_margin = attr.ib(default=15) 51 | 52 | def set_stake_xy(self): 53 | """ 54 | Sets the xy coordinates for all station stakes assigned to the 55 | alignment. 56 | """ 57 | obj = [] 58 | p = [] 59 | 60 | for x in self.stakes: 61 | if x._type == 'station': 62 | obj.append(x) 63 | p.append((x.station, x.offset, x.rotation)) 64 | 65 | p = np.array(p) 66 | c, s = np.cos(p[:,2]), np.sin(p[:,2]) 67 | c, s = np.column_stack([c, -s]), np.column_stack([s, c]) 68 | 69 | b = self.coordinates(p[:,0]) 70 | p = self.coordinates(p[:,:2]) 71 | p -= b 72 | 73 | c = np.einsum('ij,ij->i', p, c) 74 | s = np.einsum('ij,ij->i', p, s) 75 | p = np.column_stack([c, s]) 76 | p += b 77 | 78 | for a, b in zip(obj, p): 79 | a[:2] = b 80 | 81 | def pi_coordinates(self): 82 | """ 83 | Returns an array of PI coordinates of shape (N, 3). 84 | """ 85 | if not self.pis: 86 | return np.zeros((0, 3), dtype='float') 87 | return np.array(self.pis, dtype='float') 88 | 89 | def pi_radii(self): 90 | """ 91 | Returns an array of PI horizontal curve radii of shape (N,). 92 | """ 93 | return np.array([x.radius for x in self.pis], dtype='float') 94 | 95 | def azimuths(self): 96 | """ 97 | Returns an array of alignment azimuths in the shape (N,). Each element 98 | of the array corresponds to a PI index and represents the azimuth of 99 | the alignment ahead of that PI. 100 | """ 101 | if not self.pis: 102 | return np.zeros(0, dtype='float') 103 | 104 | elif len(self.pis) == 1: 105 | return np.zeros(1, dtype='float') 106 | 107 | x = self.pi_coordinates() 108 | dx = x[1:,:2] - x[:-1,:2] 109 | az = np.arctan2(dx[:,0], dx[:,1]) 110 | az = np.append(az, az[-1]) 111 | 112 | return np.asarray(az, dtype='float') 113 | 114 | def deflection_angles(self): 115 | """ 116 | Returns an array of PI deflection angles in the shape (N,). The angle 117 | is negative for turns to the left and positive for turns to the right. 118 | """ 119 | if not self.pis: 120 | return np.zeros(0, dtype='float') 121 | 122 | elif len(self.pis) == 1: 123 | return np.zeros(1, dtype='float') 124 | 125 | az = self.azimuths() 126 | da = az[1:] - az[:-1] 127 | i = (np.abs(da) > np.pi) 128 | da[i] -= 2 * np.pi * np.sign(da[i]) 129 | da = np.insert(da, 0, 0) 130 | 131 | return np.asarray(da, dtype='float') 132 | 133 | def tangent_ordinates(self): 134 | """ 135 | Returns an array of tangent ordinates corresponding to each PI 136 | in the shape (N,). This value is the horizontal distance between 137 | the PI and PC and PI and PT. 138 | """ 139 | r = self.pi_radii() 140 | da = self.deflection_angles() 141 | return r * np.abs(np.tan(da/2)) 142 | 143 | def curve_lengths(self): 144 | """ 145 | Returns an array of horizontal curve lengths corresponding to each PI 146 | in teh shape (N,). This value is the station distance between the 147 | PC and PT. 148 | """ 149 | r = self.pi_radii() 150 | da = self.deflection_angles() 151 | return r * np.abs(da) 152 | 153 | def middle_ordinates(self): 154 | """ 155 | Returns an array of middle ordinate distances corresponding to each PI 156 | in the shape (N,). This value is the horizontal distance between the 157 | MPC and midpoint of the chord line between the PC and PT. 158 | """ 159 | r = self.pi_radii() 160 | da = np.abs(self.deflection_angles()) 161 | return r * (1 - np.cos(da/2)) 162 | 163 | def external_ordinates(self): 164 | """ 165 | Returns an array of external ordinates corresponding to each PI 166 | in the shape (N,). This is the horizontal distance between the 167 | MPC and PI. 168 | """ 169 | r = self.pi_radii() 170 | da = self.deflection_angles() 171 | return r * np.abs(np.tan(da/2) * np.tan(da/4)) 172 | 173 | def chord_distances(self): 174 | """ 175 | Returns an array of chord distances corresponding to each PI 176 | in teh shape (N,). This is the straight line horizontal distance 177 | between the PC and PT. 178 | """ 179 | r = self.pi_radii() 180 | da = np.abs(self.deflection_angles()) 181 | return 2 * r * np.sin(da/2) 182 | 183 | def pt_coordinates(self): 184 | """ 185 | Returns an array of (x, y) coordinates for the Point of Tangents (PT) 186 | in the shape (N, 2). 187 | """ 188 | if not self.pis: 189 | return np.zeros((0, 3), dtype='float') 190 | 191 | pi = self.pi_coordinates() 192 | az = self.azimuths() 193 | t = self.tangent_ordinates() 194 | t = np.expand_dims(t, 1) 195 | uv = np.column_stack([np.sin(az), np.cos(az)]) 196 | pt = pi[:,:2] + t * uv 197 | 198 | return np.asarray(pt, dtype='float') 199 | 200 | def pc_coordinates(self): 201 | """ 202 | Returns an array of (x, y) coordinates for the Point of Curves (PC) 203 | in the shape (N, 2). 204 | """ 205 | if not self.pis: 206 | return np.zeros((0, 3), dtype='float') 207 | 208 | pi = self.pi_coordinates() 209 | az = self.azimuths() 210 | da = self.deflection_angles() 211 | t = self.tangent_ordinates() 212 | t = np.expand_dims(t, 1) 213 | az -= da 214 | uv = np.column_stack([np.sin(az), np.cos(az)]) 215 | pc = pi[:,:2] - t * uv 216 | 217 | return np.asarray(pc, dtype='float') 218 | 219 | def mpc_coordinates(self): 220 | """ 221 | Returns an array of (x, y) coordinates for the Midpoint of Curves (MPC) 222 | in the shape (N, 2). 223 | """ 224 | if not self.pis: 225 | return np.zeros((0, 3), dtype='float') 226 | 227 | pi = self.pi_coordinates() 228 | az = self.azimuths() 229 | da = self.deflection_angles() 230 | e = self.external_ordinates() 231 | az += (np.pi - da) / 2 232 | da = np.expand_dims(da, 1) 233 | e = np.expand_dims(e, 1) 234 | uv = np.column_stack([np.sin(az), np.cos(az)]) 235 | mpc = pi[:,:2] + np.sign(da) * e * uv 236 | 237 | return np.asarray(mpc, dtype='float') 238 | 239 | def rp_coordinates(self): 240 | """ 241 | Returns an array of (x, y) coordinates for the Radius Points (RP) 242 | in the shape (N, 2). 243 | """ 244 | if not self.pis: 245 | return np.zeros((0, 3), dtype='float') 246 | 247 | pi = self.pi_coordinates() 248 | az = self.azimuths() 249 | da = self.deflection_angles() 250 | e = self.external_ordinates() 251 | e = np.expand_dims(e, 1) 252 | r = self.pi_radii() 253 | r = np.expand_dims(r, 1) 254 | az += (np.pi - da) / 2 255 | uv = np.column_stack([np.sin(az), np.cos(az)]) 256 | da = np.expand_dims(da, 1) 257 | rp = pi[:,:2] + np.sign(da) * (e + r) * uv 258 | 259 | return np.asarray(rp, dtype='float') 260 | 261 | def pt_stations(self): 262 | """ 263 | Returns an array of (x, y) coordinates for the Point of Tangents (PT) 264 | in the shape (N, 2). 265 | """ 266 | if not self.pis: 267 | return np.zeros(0, dtype='float') 268 | 269 | x = self.pi_coordinates() 270 | tan = self.tangent_ordinates() 271 | dist = np.linalg.norm(x[:-1,:2] - x[1:,:2], axis=1) 272 | dist = np.insert(dist, 0, 0) 273 | dist += self.curve_lengths() - tan 274 | sta = np.cumsum(dist) 275 | sta[1:] -= np.cumsum(tan[:-1]) 276 | 277 | return np.asarray(sta, dtype='float') 278 | 279 | def pc_stations(self): 280 | """ 281 | Returns an array of stations for the Point of Curves (PC) in the 282 | shape (N,). 283 | """ 284 | if not self.pis: 285 | return np.zeros(0, dtype='float') 286 | 287 | sta = self.pt_stations() - self.curve_lengths() 288 | return np.asarray(sta, dtype='float') 289 | 290 | def mpc_stations(self): 291 | """ 292 | Returns an array of stations for the Midpoint of Curves (MPC) 293 | in the shape (N,). 294 | """ 295 | return 0.5 * (self.pt_stations() + self.pc_stations()) 296 | 297 | def poc_transforms(self): 298 | """ 299 | Returns the POC transforms in the shape (N, 2, 2). These transforms 300 | project (x, y) global coordinates to (offset, station) station 301 | coordinates relative to the PI angle bisector. 302 | """ 303 | az = self.azimuths() 304 | da = self.deflection_angles() 305 | l = az - da / 2 306 | t = l + np.pi / 2 307 | t = np.column_stack([np.sin(t), np.cos(t), np.sin(l), np.cos(l)]) 308 | 309 | return t.reshape(t.shape[0], 2, 2) 310 | 311 | def pot_transforms(self): 312 | """ 313 | Returns the POT transforms in the shape (N, 2, 2). These transforms 314 | project (x, y) global coordinates to (offset, station) station 315 | coordinates relative to the tangent line between PI's. 316 | """ 317 | l = self.azimuths() 318 | t = l + np.pi / 2 319 | t = np.column_stack([np.sin(t), np.cos(t), np.sin(l), np.cos(l)]) 320 | return t.reshape(t.shape[0], 2, 2) 321 | 322 | def segment_indices(self, stations): 323 | """ 324 | Determines the segment type and PI indices corresponding to the 325 | specified stations. Returns an array of shape (N, 2). The first column 326 | of the array contains 1 if the station is located along an alignment 327 | tangent or 2 if the station is located on a horizontal curve or 328 | alignment bisector. The second column contains the index corresponding 329 | to the PI where the point is located. 330 | 331 | Parameters 332 | ---------- 333 | stations : array 334 | An array of stations of shape (N,). 335 | """ 336 | sta = np.asarray(stations) 337 | pc_sta = self.pc_stations() 338 | pt_sta = self.pt_stations() 339 | s = SpatialHash(np.expand_dims(sta, 1), self.grid) 340 | 341 | # Set values beyond alignment limits 342 | r = np.zeros((sta.shape[0], 2), dtype='int') 343 | r[sta < 0] = 1, 0 344 | r[sta > pt_sta[-1]] = 1, pt_sta.shape[0]-1 345 | 346 | # POT segments 347 | ah = np.expand_dims(pc_sta[1:], 1) 348 | bk = np.expand_dims(pt_sta[:-1], 1) 349 | 350 | for i, (a, b) in enumerate(zip(ah, bk)): 351 | f = s.query_range(b, a, 0) 352 | r[f] = 1, i 353 | 354 | # POC segments 355 | f = (self.curve_lengths() == 0) 356 | pc_sta[f] -= Alignment.BISC_TOL 357 | pt_sta[f] += Alignment.BISC_TOL 358 | 359 | ah = np.expand_dims(pt_sta[1:-1], 1) 360 | bk = np.expand_dims(pc_sta[1:-1], 1) 361 | 362 | for i, (a, b) in enumerate(zip(ah, bk)): 363 | f = s.query_range(b, a, 0) 364 | r[f] = 2, i+1 365 | 366 | return r 367 | 368 | def _pot_coordinates(self, result, seg, sta_coords): 369 | """ 370 | Assigns the POT coordinates for :meth:`.coordinates`. 371 | 372 | Parameters 373 | ---------- 374 | result : array 375 | The array to which the results will be added. 376 | seg : array 377 | The segment indices array. 378 | sta_coords : array 379 | An array of station coordinates of shape (N, 2). 380 | """ 381 | f = (seg[:,0] == 1) 382 | 383 | if not f.any(): 384 | return 385 | 386 | sta = np.expand_dims(sta_coords[f,0], 1) 387 | off = np.expand_dims(sta_coords[f,1], 1) 388 | 389 | i = seg[f,1] 390 | t = self.pot_transforms()[i] 391 | tx, ty = t[:,0], t[:,1] 392 | pt_coord = self.pt_coordinates()[i] 393 | pt_sta = np.expand_dims(self.pt_stations()[i], 1) 394 | 395 | result[f] = tx * off + ty * (sta - pt_sta) + pt_coord 396 | 397 | def _poc_bisc_coordinates(self, result, seg, sta_coords): 398 | """ 399 | Assigns the POC bisector coordinates for :meth:`.coordinates`. 400 | 401 | Parameters 402 | ---------- 403 | result : array 404 | The array to which the results will be added. 405 | seg : array 406 | The segment indices array. 407 | sta_coords : array 408 | An array of station coordinates of shape (N, 2). 409 | """ 410 | f = (seg[:,0] == 2) & (self.curve_lengths() == 0)[seg[:,1]] 411 | 412 | if not f.any(): 413 | return 414 | 415 | off = np.expand_dims(sta_coords[f,1], 1) 416 | 417 | i = seg[f,1] 418 | tx = self.poc_transforms()[i,0] 419 | rp_coord = self.rp_coordinates()[i] 420 | 421 | result[f] = tx * off + rp_coord 422 | 423 | def _poc_curve_coordinates(self, result, seg, sta_coords): 424 | """ 425 | Assigns the POC curve coordinates for :meth:`.coordinates`. 426 | 427 | Parameters 428 | ---------- 429 | result : array 430 | The array to which the results will be added. 431 | seg : array 432 | The segment indices array. 433 | sta_coords : array 434 | An array of station coordinates of shape (N, 2). 435 | """ 436 | l = self.curve_lengths() 437 | f = (seg[:,0] == 2) & (l != 0)[seg[:,1]] 438 | 439 | if not f.any(): 440 | return 441 | 442 | sta = sta_coords[f,0] 443 | off = sta_coords[f,1] 444 | 445 | i = seg[f,1] 446 | tx = self.poc_transforms()[i,0] 447 | mpc_sta = self.mpc_stations()[i] 448 | rp_coord = self.rp_coordinates()[i] 449 | da = self.deflection_angles()[i] 450 | r = np.expand_dims(self.pi_radii()[i], 1) 451 | 452 | beta = da * (mpc_sta - sta) / l[i] 453 | c, s = np.cos(beta), np.sin(beta) 454 | c, s = np.column_stack([c, -s]), np.column_stack([s, c]) 455 | 456 | c = np.einsum('ij,ij->i', tx, c) 457 | s = np.einsum('ij,ij->i', tx, s) 458 | 459 | tx = np.column_stack([c, s]) 460 | da = np.sign(np.expand_dims(da, 1)) 461 | off = np.expand_dims(off, 1) 462 | 463 | result[f] = tx * (off - da * r) + rp_coord 464 | 465 | def coordinates(self, sta_coords): 466 | """ 467 | Returns the (x, y) or (x, y, z) global coordinates corresponding 468 | to the input station coordinates. Result is in the shape of (N, 2) 469 | of (N, 3). 470 | 471 | Parameters 472 | ---------- 473 | sta_coords : array 474 | An array of (station), (station, offset), or (station, offset, z) 475 | coordinates of the shape (N,), (N, 2) or (N, 3). 476 | """ 477 | sta_coords = np.asarray(sta_coords) 478 | 479 | # If shape is (N,), add zero offsets 480 | if len(sta_coords.shape) == 1: 481 | sta_coords = np.column_stack([sta_coords, np.zeros(sta_coords.shape[0])]) 482 | 483 | result = np.zeros((sta_coords.shape[0], 2), dtype='float') 484 | seg = self.segment_indices(sta_coords[:,0]) 485 | 486 | self._pot_coordinates(result, seg, sta_coords) 487 | self._poc_bisc_coordinates(result, seg, sta_coords) 488 | self._poc_curve_coordinates(result, seg, sta_coords) 489 | 490 | # Add z coordinate to result if available 491 | if sta_coords.shape[1] == 3: 492 | result = np.column_stack([result, sta_coords[:,2]]) 493 | 494 | return np.asarray(result, dtype='float') 495 | 496 | def _pot_station_coordinates(self, result, spatial_hash, coords): 497 | """ 498 | Adds the POT station coordinates within the view. 499 | 500 | Parameters 501 | ---------- 502 | result : dict 503 | The dictionary to which the results will be added. 504 | spatial_hash : array 505 | The spatial hash. 506 | coords : array 507 | An array of coordinates of shape (N, 2) or (N, 3). 508 | """ 509 | t = self.pot_transforms() 510 | pt_sta = self.pt_stations() 511 | pt_coord = self.pt_coordinates() 512 | 513 | bk = self.pt_coordinates()[:-1] 514 | ah = self.pc_coordinates()[1:] 515 | 516 | if t.shape[0] > 0: 517 | bk[0] -= self.view_margin * t[0, 1] 518 | ah[-1] += self.view_margin * t[-1, 1] 519 | 520 | for i, (a, b) in enumerate(zip(ah, bk)): 521 | f = spatial_hash.query_range(b, a, self.view_offset) 522 | 523 | if f.shape[0] == 0: 524 | continue 525 | 526 | delta = coords[f,:2] - pt_coord[i] 527 | sta = np.dot(delta, t[i,1]) + pt_sta[i] 528 | off = np.dot(delta, t[i,0]) 529 | 530 | if coords.shape[1] == 3: 531 | p = np.column_stack([sta, off, coords[f,2]]) 532 | else: 533 | p = np.column_stack([sta, off]) 534 | 535 | for n, m in enumerate(f): 536 | if m not in result: 537 | result[m] = [] 538 | result[m].append(p[n]) 539 | 540 | def _poc_station_coordinates(self, result, spatial_hash, coords): 541 | """ 542 | Adds the POC station coordinates within the view. 543 | 544 | Parameters 545 | ---------- 546 | result : dict 547 | The dictionary to which the results will be added. 548 | spatial_hash : array 549 | The spatial hash. 550 | coords : array 551 | An array of coordinates of shape (N, 2) or (N, 3). 552 | """ 553 | l = self.curve_lengths() 554 | t = self.poc_transforms() 555 | da = self.deflection_angles() 556 | pc_sta = self.pc_stations() 557 | pt_sta = self.pt_stations() 558 | rp_coord = self.rp_coordinates() 559 | pt_coord = self.pt_coordinates() 560 | 561 | for i in range(1, len(self.pis)-1): 562 | r = self.pis[i].radius 563 | ro = r + self.view_offset 564 | ri = max(r - self.view_offset, 0) 565 | f = spatial_hash.query_point(rp_coord[i], ro, ri) 566 | 567 | if f.shape[0] == 0: 568 | continue 569 | 570 | if l[i] == 0: 571 | # Angle bisector 572 | delta = coords[f,:2] - pt_coord[i] 573 | sta = np.dot(delta, t[i,1]) + pt_sta[i] 574 | off = np.dot(delta, t[i,0]) 575 | 576 | g = ((np.abs(off) <= self.view_offset) 577 | & (sta >= pt_sta[i] - Alignment.BISC_TOL) 578 | & (sta <= pt_sta[i] + Alignment.BISC_TOL)) 579 | else: 580 | # Horizontal curve 581 | delta = pt_coord[i] - rp_coord[i] 582 | delta = np.arctan2(delta[0], delta[1]) 583 | p = coords[f,:2] - rp_coord[i] 584 | delta -= np.arctan2(p[:,0], p[:,1]) 585 | 586 | sta = pt_sta[i] - (l[i] / da[i]) * delta 587 | off = np.sign(da[i]) * (r - np.linalg.norm(p, axis=1)) 588 | 589 | g = (sta >= pc_sta[i]) & (sta <= pt_sta[i]) 590 | 591 | if coords.shape[1] == 3: 592 | p = np.column_stack([sta, off, coords[f,2]])[g] 593 | else: 594 | p = np.column_stack([sta, off])[g] 595 | 596 | for n, m in enumerate(f[g]): 597 | if m not in result: 598 | result[m] = [] 599 | result[m].append(p[n]) 600 | 601 | def station_coordinates(self, coordinates): 602 | """ 603 | Finds the (station, offset) or (station, offset, z) coordinates 604 | for the input global coordinates. Returns a dictionary of point 605 | indices with arrays of shape (N, 2) or (N, 3). If a point index 606 | is not in the dictionary, then no points are located along 607 | the alignment within the view threshold. 608 | 609 | Parameters 610 | ---------- 611 | coordinates : array 612 | An array of (x, y) or (x, y, z) global coordinates in the shape 613 | (N, 2) or (N, 3). 614 | """ 615 | coordinates = np.asarray(coordinates) 616 | s = SpatialHash(coordinates[:,:2], self.grid) 617 | result = {} 618 | 619 | self._pot_station_coordinates(result, s, coordinates) 620 | self._poc_station_coordinates(result, s, coordinates) 621 | 622 | for k, x in result.items(): 623 | result[k] = np.array(x, dtype='float') 624 | 625 | return result 626 | 627 | def plot_plan(self, ax=None, step=1, symbols={}): 628 | """ 629 | Plots a the plan view for the alignment. 630 | 631 | Parameters 632 | ---------- 633 | ax : :class:`matplotlib.axes.Axes` 634 | The axex to which to add the plot. If None, a new figure and axes 635 | will be created. 636 | step : float 637 | The step interval to use for plotting points along horizontal 638 | curves. 639 | symbols : dict 640 | A dictionary of symbols to use for the plot. The following keys 641 | are used: 642 | 643 | * `pi`: PI point symbol, default is 'r.' 644 | * `rp`: RP point symbol, default is 'c.' 645 | * `pc`: PC point symbol, default is 'b.' 646 | * `pt`: PT point symbol, default is 'b.' 647 | * `alignment`: Alignment lines, default is 'b-' 648 | * `stakes`: Stake symbols, default is 'rx' 649 | 650 | Examples 651 | -------- 652 | .. plot:: ../examples/survey/alignment_ex1.py 653 | :include-source: 654 | """ 655 | if ax is None: 656 | x = self.pi_coordinates()[:,:2] 657 | mx = x.max(axis=0) 658 | c = 0.5 * (mx + x.min(axis=0)) 659 | r = 1.1 * (np.max(mx - c) + self.view_offset + self.view_margin) 660 | xlim, ylim = np.column_stack([c - r, c + r]) 661 | 662 | fig = plt.figure() 663 | ax = fig.add_subplot(111, 664 | title=self.name, 665 | xlim=xlim, 666 | ylim=ylim, 667 | xlabel='X', 668 | ylabel='Y', 669 | aspect='equal' 670 | ) 671 | ax.grid('major', alpha=0.2) 672 | 673 | sym = dict( 674 | pi='r.', 675 | rp='c.', 676 | pc='b.', 677 | pt='b.', 678 | alignment='b-', 679 | stakes='rx' 680 | ) 681 | sym.update(symbols) 682 | 683 | pt = self.pt_coordinates() 684 | pc = self.pc_coordinates() 685 | 686 | if sym['alignment'] is not None: 687 | for a, b in zip(pt[:-1], pc[1:]): 688 | x = np.array([a, b]) 689 | ax.plot(x[:,0], x[:,1], sym['alignment']) 690 | 691 | for a, b in zip(self.pt_stations(), self.pc_stations()): 692 | if a != b: 693 | n = int(np.ceil((a - b) / step)) 694 | sta = np.linspace(b, a, n) 695 | x = self.coordinates(sta) 696 | ax.plot(x[:,0], x[:,1], sym['alignment']) 697 | 698 | if sym['pi'] is not None: 699 | x = self.pi_coordinates() 700 | ax.plot(x[:,0], x[:,1], sym['pi']) 701 | 702 | if sym['rp'] is not None: 703 | x = self.rp_coordinates() 704 | ax.plot(x[:,0], x[:,1], sym['rp']) 705 | 706 | if sym['pt'] is not None: 707 | ax.plot(pt[:,0], pt[:,1], sym['pt']) 708 | 709 | if sym['pc'] is not None: 710 | ax.plot(pc[:,0], pc[:,1], sym['pc']) 711 | 712 | if sym['stakes'] is not None and len(self.stakes) > 0: 713 | self.set_stake_xy() 714 | x = np.array(self.stakes) 715 | ax.plot(x[:,0], x[:,1], sym['stakes']) 716 | 717 | return ax 718 | -------------------------------------------------------------------------------- /civpy/survey/alignment_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from .pi import * 4 | from .survey_stake import * 5 | from .alignment import * 6 | 7 | 8 | def Alignment1(): 9 | p = [ 10 | # x, y 11 | ( 0, 0), 12 | ( 10, 10), 13 | ( 10, 20), 14 | ( 0, 30), 15 | (-10, 20), 16 | (-10, 10), 17 | ( 0, 0) 18 | ] 19 | 20 | p = [PI(*x) for x in p] 21 | 22 | return Alignment('Alignment1', pis=p) 23 | 24 | 25 | def Alignment2(): 26 | p = [ 27 | # x, y z, r 28 | (-100, -200, 0, 0), 29 | (-200, -200, 0, 40), 30 | (-200, 200, 0, 40), 31 | ( 200, 200, 0, 40), 32 | ( 200, -200, 0, 40), 33 | ( 100, -200, 0, 40), 34 | ( 100, 100, 0, 40), 35 | (-100, 100, 0, 40), 36 | (-100, -100, 0, 40), 37 | ( 0, -100, 0, 40), 38 | ( 0, 0, 0, 0) 39 | ] 40 | 41 | q = [ 42 | # sta, off, z, ht, rot, 43 | ( 0, 30, 0, 0, -0.78539), 44 | ( 100, 30, 0, 0, 0), 45 | ( 300, -30, 0, 0, 0), 46 | ( 475, -30, 0, 0, 0), 47 | (1000, 0, 0, 0, 0), 48 | (1975, -30, 0, 0, 0) 49 | ] 50 | 51 | p = [PI(*x) for x in p] 52 | q = [SurveyStake.init_station(*x) for x in q] 53 | 54 | return Alignment('Alignment2', pis=p, stakes=q) 55 | 56 | 57 | def Alignment3(): 58 | p = [ 59 | # x, y, z, r 60 | ( 0, 0, 0, 0), 61 | (100, 0, 0, 10), 62 | (100, 100, 0, 10), 63 | (200, 100, 0, 0) 64 | ] 65 | 66 | p = [PI(*x) for x in p] 67 | 68 | return Alignment('Alignment3', pis=p) 69 | 70 | 71 | def Alignment4(): 72 | p = [ 73 | # x, y, z, r 74 | ( 0, 0, 0, 0), 75 | (10, 0, 0, 0), 76 | (10, 10, 0, 5), 77 | ( 0, 10, 0, 0) 78 | ] 79 | 80 | p = [PI(*x) for x in p] 81 | 82 | return Alignment('Alignment4', pis=p) 83 | 84 | 85 | def Alignment5(): 86 | p = [ 87 | # x, y, z, r 88 | (-10, -10, 0, 0), 89 | ( 10, -10, 0, 10), 90 | ( 10, 10, 0, 10), 91 | (-10, 10, 0, 10), 92 | (-10, -10, 0, 10), 93 | ( 10, -10, 0, 0), 94 | ( 10, 10, 0, 0), 95 | (-10, 10, 0, 0), 96 | (-10, -10, 0, 0) 97 | ] 98 | 99 | p = [PI(*x) for x in p] 100 | 101 | return Alignment('Alignment5', pis=p) 102 | 103 | 104 | def test_pi_coordinates(): 105 | p = np.array([ 106 | # x, y, z 107 | ( 0, 0, 0), 108 | ( 10, 10, 0), 109 | ( 10, 20, 0), 110 | ( 0, 30, 0), 111 | (-10, 20, 0), 112 | (-10, 10, 0), 113 | ( 0, 0, 0) 114 | ]).ravel() 115 | 116 | align = Alignment1() 117 | q = align.pi_coordinates().ravel() 118 | 119 | assert pytest.approx(p) == q 120 | 121 | 122 | def test_pi_radii(): 123 | align = Alignment2() 124 | a = align.pi_radii() 125 | b = np.array([0, 40, 40, 40, 40, 40, 40, 40, 40, 40, 0]) 126 | 127 | assert pytest.approx(a) == b 128 | 129 | 130 | def test_azimuths1(): 131 | align = Alignment1() 132 | a = align.azimuths() * 180/np.pi 133 | b = np.array([45, 0, -45, -135, 180, 135, 135]) 134 | 135 | assert pytest.approx(a) == b 136 | 137 | 138 | def test_azimuths2(): 139 | align = Alignment1() 140 | align.pis = list(reversed(align.pis)) 141 | 142 | a = align.azimuths() * 180/np.pi 143 | b = np.array([-45, 0, 45, 135, 180, -135, -135]) 144 | 145 | assert pytest.approx(a) == b 146 | 147 | 148 | def test_deflection_angles1(): 149 | align = Alignment1() 150 | a = align.deflection_angles() * 180/np.pi 151 | b = np.array([0, -45, -45, -90, -45, -45, 0]) 152 | 153 | assert pytest.approx(a) == b 154 | 155 | 156 | def test_deflection_angles2(): 157 | align = Alignment1() 158 | align.pis = list(reversed(align.pis)) 159 | 160 | a = align.deflection_angles() * 180/np.pi 161 | b = np.array([0, 45, 45, 90, 45, 45, 0]) 162 | 163 | assert pytest.approx(a) == b 164 | 165 | 166 | def test_curve_lengths(): 167 | align = Alignment3() 168 | a = align.curve_lengths() 169 | b = np.array([0, 15.70796326794896, 15.70796326794896, 0]) 170 | 171 | assert pytest.approx(a) == b 172 | 173 | 174 | def test_tangent_ordinates(): 175 | align = Alignment3() 176 | a = align.tangent_ordinates() 177 | b = np.array([0, 10, 10, 0]) 178 | 179 | assert pytest.approx(a) == b 180 | 181 | 182 | def test_chord_distances(): 183 | align = Alignment3() 184 | a = align.chord_distances() 185 | b = np.array([0, 14.142135623730951, 14.142135623730951, 0]) 186 | 187 | assert pytest.approx(a) == b 188 | 189 | 190 | def test_middle_ordinates(): 191 | align = Alignment3() 192 | a = align.middle_ordinates() 193 | b = np.array([0, 2.9289321881345245, 2.9289321881345245, 0]) 194 | 195 | assert pytest.approx(a) == b 196 | 197 | 198 | def test_external_ordinates(): 199 | align = Alignment3() 200 | a = align.external_ordinates() 201 | b = np.array([0, 4.142135623730951, 4.142135623730951, 0]) 202 | 203 | assert pytest.approx(a) == b 204 | 205 | 206 | def test_pc_coordinates(): 207 | align = Alignment3() 208 | a = align.pc_coordinates().ravel() 209 | b = np.array([[0, 0], [90, 0], [100, 90], [200, 100]]).ravel() 210 | 211 | assert pytest.approx(a) == b 212 | 213 | 214 | def test_pt_coordinates(): 215 | align = Alignment3() 216 | a = align.pt_coordinates().ravel() 217 | b = np.array([[0, 0], [100, 10], [110, 100], [200, 100]]).ravel() 218 | 219 | assert pytest.approx(a) == b 220 | 221 | 222 | def test_rp_coordinates(): 223 | align = Alignment3() 224 | a = align.rp_coordinates().ravel() 225 | b = np.array([[0, 0], [90, 10], [110, 90], [200, 100]]).ravel() 226 | 227 | assert pytest.approx(a) == b 228 | 229 | 230 | def test_mpc_coordinates(): 231 | align = Alignment3() 232 | a = align.mpc_coordinates().ravel() 233 | b = np.array([ 234 | [ 0, 0], 235 | [ 97.07106781, 2.92893219], 236 | [102.92893219, 97.07106781], 237 | [ 200, 100] 238 | ]).ravel() 239 | 240 | assert pytest.approx(a) == b 241 | 242 | 243 | def test_pc_stations(): 244 | align = Alignment3() 245 | a = align.pc_stations() 246 | b = np.array([0, 90, 185.70796327, 291.41592654]) 247 | 248 | assert pytest.approx(a) == b 249 | 250 | 251 | def test_pt_stations(): 252 | align = Alignment3() 253 | a = align.pt_stations() 254 | b = np.array([0, 105.70796327, 201.41592654, 291.41592654]) 255 | 256 | assert pytest.approx(a) == b 257 | 258 | 259 | def test_mpc_stations(): 260 | align = Alignment3() 261 | a = align.mpc_stations() 262 | b = np.array([0, 97.85398163, 193.5619449, 291.41592654]) 263 | 264 | assert pytest.approx(a) == b 265 | 266 | 267 | def test_segment_indices(): 268 | align = Alignment4() 269 | p = [0, 5, 10, 17, 25] 270 | a = align.segment_indices(p).ravel() 271 | b = np.array([[1, 0], [1, 0], [2, 1], [2, 2], [1, 2]]).ravel() 272 | 273 | assert pytest.approx(a) == b 274 | 275 | 276 | def test_poc_transforms(): 277 | align = Alignment4() 278 | a = align.poc_transforms().ravel() 279 | 280 | b = np.array([ 281 | [[0, -1], [1, 0]], 282 | [[0.707106781, -0.707106781], [0.707106781, 0.707106781]], 283 | [[0.707106781, 0.707106781], [-0.707106781, 0.707106781]], 284 | [[0, 1], [-1, 0]] 285 | ]).ravel() 286 | 287 | assert pytest.approx(a) == b 288 | 289 | 290 | def test_pot_transforms(): 291 | align = Alignment4() 292 | a = align.pot_transforms().ravel() 293 | 294 | b = np.array([ 295 | [[0, -1], [1, 0]], 296 | [[1, 0], [0, 1]], 297 | [[0, 1], [-1, 0]], 298 | [[0, 1], [-1, 0]] 299 | ]).ravel() 300 | 301 | assert pytest.approx(a) == b 302 | 303 | 304 | def test_coordinates(): 305 | align = Alignment5() 306 | 307 | p = np.array([ 308 | (0, 0, 1), 309 | (10, 0, 2), 310 | (17.853981633974485, 0, 3), 311 | (17.853981633974485, -20, 4), 312 | (17.853981633974485, -10, 5), 313 | (17.853981633974485, 0, 6), 314 | (17.853981633974485, 10, 7), 315 | (25.707963267948966, 0, 8), 316 | (102.83185307, 0, 9), 317 | (122.83185307, 0, 10), 318 | (102.83185307, -14.142135623730951, 11), 319 | (122.83185307, -14.142135623730951, 12) 320 | ]) 321 | 322 | a = align.coordinates(p) 323 | 324 | b = np.array([ 325 | [-10, -10, 1], 326 | [0, -10, 2], 327 | [7.07106781, -7.07106781, 3], 328 | [-7.07106781, 7.07106781, 4], 329 | [0, 0, 5], 330 | [7.07106781, -7.07106781, 6], 331 | [14.1421356, -14.1421356, 7], 332 | [10, 0, 8], 333 | [10, 10, 9], 334 | [-10, 10, 10], 335 | [0, 0, 11], 336 | [0, 0, 12] 337 | ]) 338 | 339 | assert pytest.approx(a) == b 340 | 341 | 342 | def test_station_coordinates(): 343 | np.random.seed(234098) 344 | align = Alignment5() 345 | p = np.random.uniform(-30, 30, (1000, 3)) 346 | 347 | r = [ 348 | [-10, -10, 1], 349 | [ 10, 10, 2], 350 | [ 10, -10, 3], 351 | [-10, 10, 4], 352 | [ 0, 0, 5] 353 | ] 354 | 355 | p = np.concatenate([p, r]) 356 | q = align.station_coordinates(p) 357 | assert len(q) > 0 358 | 359 | for i, x in q.items(): 360 | b = align.coordinates(x) 361 | for a in b: 362 | assert pytest.approx(a) == p[i] 363 | 364 | 365 | def test_set_stake_xy(): 366 | align = Alignment5() 367 | 368 | p = np.array([ 369 | (0, 0, 1), 370 | (10, 0, 2), 371 | (17.853981633974485, 0, 3), 372 | (17.853981633974485, -20, 4), 373 | (17.853981633974485, -10, 5), 374 | (17.853981633974485, 0, 6), 375 | (17.853981633974485, 10, 7), 376 | (25.707963267948966, 0, 8), 377 | (102.83185307, 0, 9), 378 | (122.83185307, 0, 10), 379 | (102.83185307, -14.142135623730951, 11), 380 | (122.83185307, -14.142135623730951, 12) 381 | ]) 382 | 383 | align.stakes = [SurveyStake.init_station(*x) for x in p] 384 | 385 | align.set_stake_xy() 386 | a = np.array(align.stakes).ravel() 387 | 388 | b = np.array([ 389 | [-10, -10, 1], 390 | [0, -10, 2], 391 | [7.07106781, -7.07106781, 3], 392 | [-7.07106781, 7.07106781, 4], 393 | [0, 0, 5], 394 | [7.07106781, -7.07106781, 6], 395 | [14.1421356, -14.1421356, 7], 396 | [10, 0, 8], 397 | [10, 10, 9], 398 | [-10, 10, 10], 399 | [0, 0, 11], 400 | [0, 0, 12] 401 | ]).ravel() 402 | 403 | assert pytest.approx(a) == b 404 | 405 | 406 | def test_plot_plan(): 407 | np.random.seed(238479) 408 | align = Alignment2() 409 | align.plot_plan() 410 | -------------------------------------------------------------------------------- /civpy/survey/pi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import numpy as np 6 | 7 | __all__ = ['PI'] 8 | 9 | 10 | class PI(np.ndarray): 11 | """ 12 | A class representing a point of intersection (PI) of an alignment. 13 | 14 | Parameters 15 | ---------- 16 | x, y, z : float 17 | The x, y, and z coordinates. 18 | radius : float 19 | The radius of the horizontal curve. Use zero if a curve does not 20 | exist. 21 | """ 22 | def __new__(cls, x, y, z=0, radius=0): 23 | obj = np.array([x, y, z], dtype='float').view(cls) 24 | obj.radius = radius 25 | return obj 26 | 27 | def __array_finalize__(self, obj): 28 | if obj is None: return 29 | self.radius = getattr(obj, 'radius', 0) 30 | 31 | def x(): 32 | def fget(self): 33 | return self[0] 34 | def fset(self, value): 35 | self[0] = value 36 | return locals() 37 | x = property(**x()) 38 | 39 | def y(): 40 | def fget(self): 41 | return self[1] 42 | def fset(self, value): 43 | self[1] = value 44 | return locals() 45 | y = property(**y()) 46 | 47 | def z(): 48 | def fget(self): 49 | return self[2] 50 | def fset(self, value): 51 | self[2] = value 52 | return locals() 53 | z = property(**z()) 54 | 55 | def __repr__(self): 56 | s = [ 57 | ('x', self.x), 58 | ('y', self.y), 59 | ('z', self.z), 60 | ('radius', self.radius), 61 | ] 62 | 63 | s = ', '.join('{}={!r}'.format(x, y) for x, y in s) 64 | return '{}({})'.format(type(self).__name__, s) 65 | -------------------------------------------------------------------------------- /civpy/survey/pi_test.py: -------------------------------------------------------------------------------- 1 | from .pi import * 2 | 3 | 4 | def test_init(): 5 | p = PI(1, 2, 3, 4) 6 | 7 | assert p.x == 1 8 | assert p.y == 2 9 | assert p.z == 3 10 | assert p.radius == 4 11 | 12 | 13 | def test_repr(): 14 | p = PI(1, 2, 3, 4) 15 | repr(p) 16 | -------------------------------------------------------------------------------- /civpy/survey/spatial_hash.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from mpl_toolkits.mplot3d.art3d import Poly3DCollection 8 | from matplotlib.patches import Rectangle 9 | 10 | __all__ = ['SpatialHash'] 11 | 12 | 13 | class SpatialHash(object): 14 | """ 15 | A class representing a spatial hash structure for efficient distance 16 | queries. 17 | 18 | Parameters 19 | ---------- 20 | points : list 21 | A list of points of shape (N, D). 22 | grid : float 23 | The width of each spatial hash grid element. For 2D spaces, this value 24 | represents the width and height of each square spatial hash partition. 25 | 26 | Examples 27 | -------- 28 | The below example uses the :meth:`.query_point` and :meth:`.query_range` 29 | methods to search for points within the specified offset of a point and 30 | within the specified offset of a range, respectively. The found 31 | points are shown in green. 32 | 33 | .. plot:: ../examples/survey/spatial_hash_ex2.py 34 | :include-source: 35 | """ 36 | def __init__(self, points, grid): 37 | self._grid = grid 38 | self._dict = {} 39 | self._add_points(points) 40 | 41 | def __repr__(self): 42 | return '{}({}, grid={!r})'.format(type(self).__name__, self.points.shape, self._grid) 43 | 44 | def _check_shape(self, point): 45 | """ 46 | Checks that the input point conforms to the hash dimensionality. 47 | 48 | Parameters 49 | ---------- 50 | point : array 51 | An array of shape (D,). 52 | """ 53 | if point.shape[0] != self._dim: 54 | raise ValueError('Point is {}D but should be {}D.' 55 | .format(point.shape[0], self._dim)) 56 | 57 | def _add_points(self, points): 58 | """ 59 | Adds the input list of points to the spatial hash. 60 | 61 | Parameters 62 | ---------- 63 | points : list 64 | A list of points of shape (N, D). 65 | """ 66 | points = np.asarray(points) 67 | self._dim = points.shape[1] 68 | self.points = points 69 | 70 | hashes = self._multi_hash(points, norm=True) 71 | odict = self._dict 72 | 73 | for i, h in enumerate(hashes): 74 | if h not in odict: 75 | odict[h] = [] 76 | odict[h].append(i) 77 | 78 | def _multi_hash(self, points, norm): 79 | """ 80 | Returns a list of dictionary hash keys corresponding to the input 81 | points. 82 | 83 | Parameters 84 | ---------- 85 | points : list 86 | A list of points of shape (N, D). 87 | norm : bool 88 | If True, normalizes the points to their grid index. Otherwise, 89 | assumes that the input points are grid indices. 90 | """ 91 | if norm: 92 | points = np.asarray(points) // self._grid 93 | 94 | return (hash(tuple(x)) for x in points) 95 | 96 | def _hash(self, point, norm): 97 | """ 98 | Returns the hash key corresponding to the input point. 99 | 100 | Parameters 101 | ---------- 102 | point : list 103 | A list of shape (D,). 104 | norm : bool 105 | If True, normalizes the points to their grid index. Otherwise, 106 | assumes that the input points are grid indices. 107 | """ 108 | if norm: 109 | point = np.asarray(point) // self._grid 110 | 111 | return hash(tuple(point)) 112 | 113 | def multi_get(self, points, norm=True): 114 | """ 115 | Returns the point indices corresponding to the input array of points. 116 | 117 | Parameters 118 | ---------- 119 | points : list 120 | A list of points of shape (N, D). 121 | norm : bool 122 | If True, normalizes the points to their grid index. Otherwise, 123 | assumes that the input points are grid indices. 124 | """ 125 | result = [] 126 | odict = self._dict 127 | 128 | for x in self._multi_hash(points, norm): 129 | result.extend(odict.get(x, [])) 130 | 131 | return np.asarray(np.unique(result), dtype='int') 132 | 133 | def get(self, point, norm=True): 134 | """ 135 | Returns the point indices correesponding to the same hash as the input 136 | point. 137 | 138 | Parameters 139 | ---------- 140 | point : list 141 | A list of shape (D,). 142 | norm : bool 143 | If True, normalizes the points to their grid index. Otherwise, 144 | assumes that the input points are grid indices. 145 | """ 146 | point = self._hash(point, norm) 147 | return self._dict.get(point, []) 148 | 149 | def _query_point_hash(self, point, ro, ri): 150 | # Calculate worst case offsets 151 | diag = self._grid * self._dim**0.5 152 | ri = max(ri - diag, 0) 153 | ro = ro + diag 154 | 155 | # Create meshgrid of hash indices 156 | p = np.column_stack([point - ro, point + ro]) // self._grid 157 | p = [np.arange(a, b+1) for a, b in p] 158 | p = np.array(np.meshgrid(*p), dtype='int').T.reshape(-1, self._dim) 159 | 160 | # Filter hashes by distance 161 | dist = np.linalg.norm(point - self._grid * p, axis=1) 162 | 163 | if ri == 0: 164 | p = p[dist <= ro] 165 | else: 166 | p = p[(dist <= ro) & (dist >= ri)] 167 | 168 | return self.multi_get(p, norm=False) 169 | 170 | def query_point(self, point, ro, ri=0): 171 | """ 172 | Returns an array of point indices for all points contained within 173 | the specified inner and outer radii from the input point. 174 | 175 | Parameters 176 | ---------- 177 | point : list 178 | A list of shape (D,). 179 | ro : float 180 | The outer radius beyond which points will be excluded. 181 | ri : float 182 | The inner radius before which points will be excluded. 183 | """ 184 | point = np.asarray(point) 185 | self._check_shape(point) 186 | 187 | # Get hash filtered points 188 | result = self._query_point_hash(point, ro, ri) 189 | p = self.points[result] 190 | 191 | # Filter points by distance 192 | dist = np.linalg.norm(p - point, axis=1) 193 | 194 | if ri == 0: 195 | f = (dist <= ro) 196 | else: 197 | f = (dist <= ro) & (dist >= ri) 198 | 199 | return result[f][dist[f].argsort()] 200 | 201 | def _query_range_hash(self, a, b, ro, ri, u, l): 202 | # Calculate worst case offsets 203 | diag = self._grid * self._dim**0.5 204 | ri = max(ri - diag, 0) 205 | ro = ro + diag 206 | 207 | # Create meshgrid of hash indices 208 | x = np.column_stack([a - ro, a - ro]).min(axis=1) 209 | y = np.column_stack([b + ro, b + ro]).max(axis=1) 210 | 211 | p = np.column_stack([x, y]) // self._grid 212 | p = [np.arange(x, y+1) for x, y in p] 213 | p = np.array(np.meshgrid(*p), dtype='int').T.reshape(-1, self._dim) 214 | 215 | # Filter hashes by projection and offset 216 | v = self._grid * p - b 217 | proj = np.dot(v, u) 218 | dist = np.linalg.norm(v - proj.reshape(-1, 1) * u, axis=1) 219 | del v 220 | 221 | if ri == 0: 222 | p = p[(proj >= -diag) & (proj <= l+diag) & (dist <= ro)] 223 | else: 224 | p = p[(proj >= -diag) & (proj <= l+diag) & (dist <= ro) & (dist >= ri)] 225 | 226 | return self.multi_get(p, norm=False) 227 | 228 | def query_range(self, a, b, ro, ri=0): 229 | """ 230 | Returns an array of point indices for all points along the specified 231 | range within the inner and outer offsets. 232 | 233 | Parameters 234 | ---------- 235 | a : list 236 | The starting point for the range. The point should be of shape (D,). 237 | b : list 238 | The ending point for the range. The point should be of shape (D,). 239 | ro : float 240 | The outer offset beyond which points will be excluded. 241 | ri : float 242 | The inner offset before which points will be excluded. 243 | """ 244 | a = np.asarray(a) 245 | b = np.asarray(b) 246 | self._check_shape(a) 247 | self._check_shape(b) 248 | 249 | # Create unit vector for range 250 | u = a - b 251 | l = np.linalg.norm(u) 252 | 253 | if l == 0: 254 | return self.query_point(a, ro, ri) 255 | 256 | u = u / l 257 | 258 | # Get hash filtered points 259 | result = self._query_range_hash(a, b, ro, ri, u, l) 260 | p = self.points[result] 261 | 262 | # Filter points by projection and offset 263 | v = p - b 264 | proj = np.dot(v, u) 265 | dist = np.linalg.norm(v - proj.reshape(-1, 1) * u, axis=1) 266 | 267 | if ri == 0: 268 | f = (proj >= 0) & (proj <= l) & (dist <= ro) 269 | else: 270 | f = (proj >= 0) & (proj <= l) & (dist <= ro) & (dist >= ri) 271 | 272 | return result[f][dist[f].argsort()] 273 | 274 | def _plot_1d(self, ax, sym): 275 | """ 276 | Creates a 1D plot. 277 | 278 | Parameters 279 | ---------- 280 | ax : :class:`matplotlib.axes.Axes` 281 | The axes to which the plot will be added. If None, a new figure 282 | and axes will be created. 283 | sym : dict 284 | A dictionary of plot symbols with any of the following keys: 285 | 286 | * points: Point symbols, default is 'r.' 287 | * hash: Hash region color, default is 'b' 288 | """ 289 | # Create plot 290 | if ax is None: 291 | lim = np.array([self.points.min(), self.points.max()]) 292 | lim = self._grid * (lim // self._grid + [-1, 2]) 293 | ticks = np.arange(lim[0], lim[1] + self._grid, self._grid) 294 | 295 | fig = plt.figure() 296 | ax = fig.add_subplot(111, 297 | xlim=lim, 298 | ylim=self._grid * np.array([-0.5, 0.5]), 299 | xticks=ticks, 300 | yticks=[0] 301 | ) 302 | ax.grid('major', alpha=0.4) 303 | 304 | # Plot hash regions 305 | if sym['hash'] is not None: 306 | y = -0.5 * self._grid 307 | xs = self._grid * (self.points // self._grid) 308 | xs = set(map(tuple, xs)) 309 | 310 | for x in xs: 311 | rect = Rectangle((x[0], y), self._grid, self._grid, 312 | color=sym['hash'], 313 | alpha=0.2, 314 | zorder=1 315 | ) 316 | ax.add_artist(rect) 317 | 318 | # Plot points 319 | if sym['points'] is not None: 320 | x = self.points 321 | ax.plot(x[:,0], np.zeros(x.shape[0]), sym['points']) 322 | 323 | return ax 324 | 325 | 326 | def _plot_2d(self, ax, sym): 327 | """ 328 | Creates a 2D plot. 329 | 330 | Parameters 331 | ---------- 332 | ax : :class:`matplotlib.axes.Axes` 333 | The axes to which the plot will be added. If None, a new figure 334 | and axes will be created. 335 | sym : dict 336 | A dictionary of plot symbols with any of the following keys: 337 | 338 | * points: Point symbols, default is 'r.' 339 | * hash: Hash region color, default is 'b' 340 | """ 341 | # Create plot 342 | if ax is None: 343 | lim = np.array([self.points.min(), self.points.max()]) 344 | lim = self._grid * (lim // self._grid + [-1, 2]) 345 | ticks = np.arange(lim[0], lim[1] + self._grid, self._grid) 346 | 347 | fig = plt.figure() 348 | ax = fig.add_subplot(111, 349 | xlim=lim, 350 | ylim=lim, 351 | xticks=ticks, 352 | yticks=ticks, 353 | aspect='equal' 354 | ) 355 | ax.grid('major', alpha=0.4) 356 | 357 | # Plot hash squares 358 | if sym['hash'] is not None: 359 | xs = self._grid * (self.points // self._grid) 360 | xs = set(map(tuple, xs)) 361 | 362 | for x in xs: 363 | rect = Rectangle(x, self._grid, self._grid, 364 | color=sym['hash'], 365 | fill=False, 366 | zorder=1 367 | ) 368 | ax.add_artist(rect) 369 | 370 | # Plot points 371 | if sym['points'] is not None: 372 | x = self.points 373 | ax.plot(x[:,0], x[:,1], sym['points']) 374 | 375 | return ax 376 | 377 | def _plot_3d(self, ax, sym): 378 | """ 379 | Creats a 3D plot. 380 | 381 | Parameters 382 | ---------- 383 | ax : :class:`matplotlib.axes.Axes` 384 | The axes to which the plot will be added. If None, a new figure 385 | and axes will be created. 386 | sym : dict 387 | A dictionary of plot symbols with any of the following keys: 388 | 389 | * points: Point symbols, default is 'r.' 390 | * hash: Hash region color, default is 'b' 391 | """ 392 | # Create plot 393 | if ax is None: 394 | lim = np.array([self.points.min(), self.points.max()]) 395 | lim = self._grid * (lim // self._grid + [-1, 2]) 396 | ticks = np.arange(lim[0], lim[1] + self._grid, self._grid) 397 | 398 | fig = plt.figure() 399 | ax = fig.add_subplot(111, 400 | projection='3d', 401 | xlim=lim, 402 | ylim=lim, 403 | zlim=lim, 404 | xticks=ticks, 405 | yticks=ticks, 406 | zticks=ticks, 407 | aspect='equal' 408 | ) 409 | 410 | # Plot hash cubes 411 | if sym['hash'] is not None: 412 | xs = self._grid * (self.points // self._grid) 413 | xs = set(map(tuple, xs)) 414 | 415 | cube = [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], 416 | [0, 0, 1], [0, 1, 1], [1, 1, 1], [1, 0, 1]] 417 | simplices = [[0, 1, 2, 3], [0, 1, 5, 4], [4, 5, 6, 7], 418 | [5, 1, 2, 6], [6, 7, 3, 2], [4, 7, 3, 0]] 419 | cube = self._grid * np.array(cube) 420 | 421 | for x in xs: 422 | x = cube + x 423 | x = [[x[i] for i in s] for s in simplices] 424 | poly = Poly3DCollection(x, alpha=0.05) 425 | poly.set_facecolor(sym['hash']) 426 | poly.set_edgecolor(sym['hash']) 427 | ax.add_collection(poly) 428 | 429 | # Plot points 430 | if sym['points'] is not None: 431 | x = self.points 432 | ax.plot(x[:,0], x[:,1], x[:,2], sym['points']) 433 | 434 | return ax 435 | 436 | def plot(self, ax=None, symbols={}): 437 | """ 438 | Creates a plot of the spatial hash. Cannot create plots for hashes 439 | greater than 3 dimensions. 440 | 441 | Parameters 442 | ---------- 443 | ax : :class:`matplotlib.axes.Axes` 444 | The axes to which the plot will be added. If None, a new figure 445 | and axes will be created. 446 | symbols : dict 447 | A dictionary of plot symbols with any of the following keys: 448 | 449 | * points: Point symbols, default is 'r.' 450 | * hash: Hash region color, default is 'b' 451 | 452 | Examples 453 | -------- 454 | .. plot:: ../examples/survey/spatial_hash_ex1.py 455 | :include-source: 456 | """ 457 | # Plot symbols 458 | sym = dict( 459 | points='r.', 460 | hash='b' 461 | ) 462 | sym.update(symbols) 463 | 464 | if self._dim == 1: 465 | return self._plot_1d(ax, sym) 466 | elif self._dim == 2: 467 | return self._plot_2d(ax, sym) 468 | elif self._dim == 3: 469 | return self._plot_3d(ax, sym) 470 | else: 471 | raise ValueError('Hash is {}D but plot only supports 1D, 2D, or 3D.' 472 | .format(self._dim)) 473 | -------------------------------------------------------------------------------- /civpy/survey/spatial_hash_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from .spatial_hash import * 4 | 5 | 6 | def SpatialHash1(): 7 | np.random.seed(1283943) 8 | x = np.random.uniform(-60, 60, (2500, 2)) 9 | return SpatialHash(x, 5) 10 | 11 | 12 | def test_init(): 13 | s = SpatialHash1() 14 | 15 | # Test that values were set properly 16 | assert s.points.shape == (2500, 2) 17 | assert s._dim == 2 18 | assert s._grid == 5 19 | 20 | 21 | def test_repr(): 22 | s = SpatialHash1() 23 | repr(s) 24 | 25 | 26 | def test_bad_point(): 27 | s = SpatialHash1() 28 | 29 | with pytest.raises(ValueError): 30 | s.query_point([0, 0, 0], 25) 31 | 32 | 33 | def test_query_point(): 34 | s = SpatialHash1() 35 | x = np.array([0, 0]) 36 | 37 | i = s.query_point(x, 25, 10) 38 | p = s.points[i] 39 | 40 | dist = np.linalg.norm(x - s.points, axis=1) 41 | j = (dist <= 25) & (dist >= 10) 42 | q = s.points[j][dist[j].argsort()] 43 | 44 | assert (p == q).all() 45 | 46 | 47 | def test_query_range_same_point(): 48 | s = SpatialHash1() 49 | x = np.array([0, 0]) 50 | 51 | i = s.query_range(x, x, 25, 10) 52 | p = s.points[i] 53 | 54 | dist = np.linalg.norm(x - s.points, axis=1) 55 | j = (dist <= 25) & (dist >= 10) 56 | q = s.points[j][dist[j].argsort()] 57 | 58 | assert (p == q).all() 59 | 60 | 61 | def test_query_range(): 62 | a = np.array([-30, -30]) 63 | b = np.array([30, 30]) 64 | s = SpatialHash1() 65 | 66 | i = s.query_range(a, b, 25, 10) 67 | q = s.points[i] 68 | 69 | unit = b - a 70 | length = np.linalg.norm(unit) 71 | unit = unit / length 72 | 73 | p = s.points - a 74 | proj = np.array([np.dot(x, unit) for x in p]) 75 | proj = np.expand_dims(proj, 1) 76 | 77 | p = proj * unit + a 78 | off = np.linalg.norm(p - s.points, axis=1) 79 | proj = proj.ravel() 80 | 81 | j = (off <= 25) & (off >= 10) & (proj >= 0) & (proj <= length) 82 | p = s.points[j][off[j].argsort()] 83 | 84 | assert (p == q).all() 85 | 86 | 87 | def test_plot_1d(): 88 | np.random.seed(1283943) 89 | x = np.random.uniform(-60, 60, (2500, 1)) 90 | s = SpatialHash(x, 5) 91 | s.plot() 92 | 93 | 94 | def test_plot_2d(): 95 | s = SpatialHash1() 96 | s.plot() 97 | 98 | 99 | def test_plot_3d(): 100 | np.random.seed(1283943) 101 | x = np.random.uniform(-60, 60, (2500, 3)) 102 | s = SpatialHash(x, 5) 103 | s.plot() 104 | 105 | 106 | def test_plot_invalid_dim(): 107 | np.random.seed(1283943) 108 | x = np.random.uniform(-60, 60, (2500, 4)) 109 | s = SpatialHash(x, 5) 110 | 111 | with pytest.raises(ValueError): 112 | s.plot() 113 | -------------------------------------------------------------------------------- /civpy/survey/survey_point.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import numpy as np 6 | 7 | __all__ = ['SurveyPoint'] 8 | 9 | 10 | class SurveyPoint(np.ndarray): 11 | """ 12 | A class representing a survey point. 13 | 14 | Parameters 15 | ---------- 16 | x, y, z : float 17 | The x, y, and z coordinates. 18 | """ 19 | def __new__(cls, x, y, z, **kwargs): 20 | obj = np.array([x, y, z], dtype='float').view(cls) 21 | obj.meta = dict(**kwargs) 22 | return obj 23 | 24 | def __array_finalize__(self, obj): 25 | if obj is None: return 26 | self.meta = getattr(obj, 'meta', {}) 27 | 28 | def x(): 29 | def fget(self): 30 | return self[0] 31 | def fset(self, value): 32 | self[0] = value 33 | return locals() 34 | x = property(**x()) 35 | 36 | def y(): 37 | def fget(self): 38 | return self[1] 39 | def fset(self, value): 40 | self[1] = value 41 | return locals() 42 | y = property(**y()) 43 | 44 | def z(): 45 | def fget(self): 46 | return self[2] 47 | def fset(self, value): 48 | self[2] = value 49 | return locals() 50 | z = property(**z()) 51 | 52 | def __repr__(self): 53 | s = [ 54 | ('x', self.x), 55 | ('y', self.y), 56 | ('z', self.z), 57 | ('meta', self.meta), 58 | ] 59 | 60 | s = ', '.join('{}={!r}'.format(x, y) for x, y in s) 61 | return '{}({})'.format(type(self).__name__, s) 62 | -------------------------------------------------------------------------------- /civpy/survey/survey_point_test.py: -------------------------------------------------------------------------------- 1 | from .survey_point import * 2 | 3 | 4 | def test_init(): 5 | p = SurveyPoint(1, 2, 3) 6 | 7 | assert p.x == 1 8 | assert p.y == 2 9 | assert p.z == 3 10 | assert p.meta == {} 11 | 12 | def test_repr(): 13 | p = SurveyPoint(1, 2, 3) 14 | repr(p) 15 | -------------------------------------------------------------------------------- /civpy/survey/survey_stake.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import numpy as np 6 | 7 | __all__ = ['SurveyStake'] 8 | 9 | 10 | class SurveyStake(np.ndarray): 11 | """ 12 | A class representing a survey stake. This method should be initialized 13 | using the :meth:`SurveyStake.init_xy` or :meth:`SurveyStake.init_station` 14 | class methods. 15 | """ 16 | XY = 'xy' 17 | STATION = 'station' 18 | TYPES = (XY, STATION) 19 | 20 | def __new__(cls, x, y, z, station, offset, height, rotation, lock_z, _type, 21 | _init=False, **kwargs): 22 | if not _init: 23 | raise ValueError('SurveyStake should be initialized using the ' 24 | 'SurveyStake.init_xy or SurveyStake.init_station methods ' 25 | 'in lieu of the standard initializer.') 26 | 27 | obj = np.array([x, y, z], dtype='float').view(cls) 28 | obj.station = station 29 | obj.offset = offset 30 | obj.height = height 31 | obj.rotation = rotation 32 | obj.lock_z = lock_z 33 | obj._type = _type 34 | obj.meta = dict(**kwargs) 35 | return obj 36 | 37 | def __array_finalize__(self, obj): 38 | if obj is None: return 39 | self.station = getattr(obj, 'station', 0) 40 | self.offset = getattr(obj, 'offset', 0) 41 | self.height = getattr(obj, 'height', 0) 42 | self.rotation = getattr(obj, 'rotation', 0) 43 | self.lock_z = getattr(obj, 'lock_z', False) 44 | self._type = getattr(obj, '_type', 'xy') 45 | self.meta = getattr(obj, 'meta', {}) 46 | 47 | def x(): 48 | def fget(self): 49 | return self[0] 50 | def fset(self, value): 51 | self[0] = value 52 | return locals() 53 | x = property(**x()) 54 | 55 | def y(): 56 | def fget(self): 57 | return self[1] 58 | def fset(self, value): 59 | self[1] = value 60 | return locals() 61 | y = property(**y()) 62 | 63 | def z(): 64 | def fget(self): 65 | return self[2] 66 | def fset(self, value): 67 | self[2] = value 68 | return locals() 69 | z = property(**z()) 70 | 71 | def _type(): 72 | def fget(self): 73 | return self.__type 74 | def fset(self, value): 75 | if value not in self.TYPES: 76 | raise ValueError('Type {!r} must be in {!r}.'.format(value, self.TYPES)) 77 | self.__type = value 78 | return locals() 79 | _type = property(**_type()) 80 | 81 | def __repr__(self): 82 | s = [ 83 | ('_type', self._type), 84 | ('x', self.x), 85 | ('y', self.y), 86 | ('z', self.z), 87 | ('station', self.station), 88 | ('offset', self.offset), 89 | ('lock_z', self.lock_z), 90 | ('meta', self.meta), 91 | ] 92 | 93 | s = ', '.join('{}={!r}'.format(x, y) for x, y in s) 94 | return '{}({})'.format(type(self).__name__, s) 95 | 96 | @classmethod 97 | def init_xy(cls, x, y, z=0, height=0, rotation=0, lock_z=False, **kwargs): 98 | """ 99 | Initializes a survey stake based on an (x, y) global coordinate. 100 | 101 | Parameters 102 | ---------- 103 | x, y, z : float 104 | The x, y, and z coordinates. 105 | height : float 106 | The height of the point above z. 107 | rotation : float 108 | The rotation of the point about its base point. 109 | lock_z : float 110 | If False, the alignment will be snapped to the TIN (if applicable) 111 | during certain updates. Otherwise, the z coordinate will remain 112 | fixed. 113 | """ 114 | return cls( 115 | x=x, y=y, z=z, 116 | station=0, 117 | offset=0, 118 | height=height, 119 | rotation=rotation, 120 | lock_z=lock_z, 121 | _type='xy', 122 | _init=True, 123 | **kwargs 124 | ) 125 | 126 | @classmethod 127 | def init_station(cls, station, offset=0, z=0, height=0, rotation=0, 128 | lock_z=False, **kwargs): 129 | """ 130 | Initializes a survey stake based on a survey station and offset. 131 | 132 | Parameters 133 | ---------- 134 | station : float 135 | The alignment survey station. 136 | offset : float 137 | The offset from the alignment. 138 | z : float 139 | The z coordinate. 140 | height : float 141 | The height of the point above z. 142 | rotation : float 143 | The rotation of the point about its base point. 144 | lock_z : float 145 | If False, the alignment will be snapped to the TIN (if applicable) 146 | during certain updates. Otherwise, the z coordinate will remain 147 | fixed. 148 | """ 149 | return cls( 150 | x=0, y=0, z=z, 151 | height=height, 152 | rotation=rotation, 153 | station=station, 154 | offset=offset, 155 | lock_z=lock_z, 156 | _type='station', 157 | _init=True, 158 | **kwargs 159 | ) 160 | -------------------------------------------------------------------------------- /civpy/survey/survey_stake_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .survey_stake import * 3 | 4 | 5 | def test_init(): 6 | args = [0] * 10 7 | 8 | with pytest.raises(ValueError): 9 | SurveyStake(*args) 10 | 11 | 12 | def test_repr(): 13 | p = SurveyStake.init_xy(1, 2, 3) 14 | repr(p) 15 | 16 | 17 | def test_init_xy(): 18 | p = SurveyStake.init_xy( 19 | x=1, 20 | y=2, 21 | z=3 22 | ) 23 | 24 | assert p.x == 1 25 | assert p.y == 2 26 | assert p.z == 3 27 | assert p.lock_z == False 28 | assert p._type == 'xy' 29 | assert p.height == 0 30 | assert p.rotation == 0 31 | 32 | 33 | def test_init_station(): 34 | p = SurveyStake.init_station( 35 | station=1, 36 | offset=2, 37 | z=3 38 | ) 39 | 40 | assert p.station == 1 41 | assert p.offset == 2 42 | assert p.z == 3 43 | assert p.lock_z == False 44 | assert p._type == 'station' 45 | assert p.height == 0 46 | assert p.rotation == 0 47 | -------------------------------------------------------------------------------- /civpy/survey/tin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019, Matt Pewsey 3 | """ 4 | 5 | import attr 6 | import numpy as np 7 | from math import ceil 8 | import matplotlib.pyplot as plt 9 | from scipy.spatial import Delaunay 10 | from .spatial_hash import SpatialHash 11 | 12 | __all__ = ['TIN'] 13 | 14 | 15 | @attr.s(hash=False) 16 | class TIN(object): 17 | """ 18 | A class for creating triangulated irregular networks (TIN) models for 19 | 3D surfaces. Includes methods for performing elevation and distance queries. 20 | 21 | Parameters 22 | ---------- 23 | name : str 24 | The name of the model. 25 | points : array 26 | An array of points of shape (N, 3). 27 | breaklines : list 28 | A list of arrays of points representing breakline polylines. 29 | max_edge : float 30 | The maximum edge length beyond which simplices will not be included 31 | in the triangulation. If None, no simplices will be removed. 32 | step : float 33 | The setup interval for generated points along breaklines. 34 | grid : float 35 | The grid spacing used for the created spatial hash. 36 | 37 | Examples 38 | -------- 39 | The following example creates a TIN model in the shape of a pyramid 40 | then performs distances queries to its surface: 41 | 42 | .. plot:: ../examples/survey/tin_ex1.py 43 | :include-source: 44 | """ 45 | name = attr.ib() 46 | points = attr.ib(default=[], converter=np.asarray) 47 | breaklines = attr.ib(default=[]) 48 | max_edge = attr.ib(default=None) 49 | step = attr.ib(default=0.1) 50 | grid = attr.ib(default=10) 51 | 52 | def __attrs_post_init__(self): 53 | self._create_triangulation() 54 | 55 | def _create_triangulation(self): 56 | """ 57 | Creates the Delaunay trianguation and spatial hash. 58 | 59 | Parameters 60 | ---------- 61 | points : array 62 | An array of points of shape (N, 3). 63 | grid : float 64 | The spatial hash grid spacing. 65 | """ 66 | points = self.points 67 | b = self.breakpoints() 68 | 69 | if b.shape[0] > 0: 70 | if points.shape[0] > 0: 71 | points = points[points[:,2].argsort()[::-1]] 72 | points = np.concatenate([b, points]) 73 | else: 74 | points = b 75 | 76 | self.points = points 77 | self.tri = Delaunay(points[:,:2]) 78 | self.hash = SpatialHash(points[:,:2], self.grid) 79 | self._remove_simplices() 80 | 81 | def _remove_simplices(self): 82 | """ 83 | Removes all simplices with any edge greater than the max edge. 84 | """ 85 | if self.max_edge is not None: 86 | p = self.tri.points 87 | s = self.tri.simplices 88 | 89 | a, b, c = p[s[:,0]], p[s[:,1]], p[s[:,2]] 90 | 91 | f = ((np.linalg.norm(a - b) <= self.max_edge) 92 | & (np.linalg.norm(b - c) <= self.max_edge) 93 | & (np.linalg.norm(a - c) <= self.max_edge)) 94 | 95 | self.tri.simplices = s[f] 96 | 97 | def breakpoints(self): 98 | """ 99 | Returns an array of breakpoints for the assigned breaklines. The 100 | breakpoints are sorted by z coordinate from greatest to least. 101 | """ 102 | points = [np.zeros((0, 3), dtype='float')] 103 | 104 | for line in self.breaklines: 105 | line = np.asarray(line) 106 | 107 | for i, (a, b) in enumerate(zip(line[:-1], line[1:])): 108 | m = a - b 109 | n = int(ceil(np.linalg.norm(m) / self.step)) 110 | if n < 2: n = 2 if i == 0 else 1 111 | x = np.expand_dims(np.linspace(0, 1, n), 1) 112 | y = m * x + b 113 | points.append(y) 114 | 115 | points = np.concatenate(points) 116 | points = points[points[:,2].argsort()[::-1]] 117 | 118 | return points 119 | 120 | def find_simplices(self, points): 121 | """ 122 | Finds the simplices which contain the (x, y) point. Returns the simplex 123 | index if a single point is input or an array of simplex indices if 124 | multiple points are input. If the returned simplex index is -1, then 125 | the (x, y) point is not contained within any simplices. 126 | 127 | Parameters 128 | ---------- 129 | points : array 130 | An array of points of shape (2,), (3,), (N, 2) or (N, 3). 131 | """ 132 | points = np.asarray(points) 133 | 134 | if len(points.shape) == 1: 135 | points = points[:2] 136 | else: 137 | points = points[:,:2] 138 | 139 | return self.tri.find_simplex(points) 140 | 141 | def _simplex_indices(self, indexes): 142 | """ 143 | Returns the simplex indices that include the input point indices. 144 | 145 | Parameters 146 | ---------- 147 | indexes : list 148 | A list of point indices for which connected simplices will be 149 | returned. 150 | """ 151 | indexes = set(indexes) 152 | a = [] 153 | 154 | for i, x in enumerate(self.tri.simplices): 155 | for j in x: 156 | if j in indexes: 157 | a.append(i) 158 | 159 | a = np.unique(a).astype('int') 160 | return a 161 | 162 | def query_simplices(self, point, radius): 163 | """ 164 | Returns the indices of all simplices that have a corner within the 165 | specified radius of the input point. 166 | 167 | Parameters 168 | ---------- 169 | point : array 170 | A point of shape (2,) or (3,). 171 | radius : float 172 | The xy-plane radius used for the query. 173 | """ 174 | point = np.asarray(point) 175 | i = self.hash.query_point(point[:2], radius) 176 | return self._simplex_indices(i) 177 | 178 | def normal(self, simplex): 179 | """ 180 | Returns the normal vector for the specified simplex. The returned 181 | normal vector is also a unit vector. 182 | 183 | Parameters 184 | ---------- 185 | simplex : int 186 | The index of the simplex. 187 | """ 188 | p = self.points 189 | s = self.tri.simplices[simplex] 190 | a, b, c = p[s[0]], p[s[1]], p[s[2]] 191 | n = np.cross(b - a, c - a) 192 | return n / np.linalg.norm(n) 193 | 194 | def elevation(self, point): 195 | """ 196 | Returns the elevation of the TIN surface at the input point. Returns 197 | NaN if the TIN surface does not exist at that point. 198 | 199 | Parameters 200 | ---------- 201 | point : array 202 | A point for which the elevation will be calculated. The point 203 | must be of shape (2,) or (3,). 204 | """ 205 | point = np.asarray(point) 206 | s = self.find_simplices(point) 207 | 208 | if s == -1: 209 | return np.nan 210 | 211 | p = self.points 212 | n = self.normal(s) 213 | s = self.tri.simplices[s] 214 | 215 | if n[2] != 0: 216 | # Plane elevation 217 | a = p[s[0]] 218 | a = np.dot(n, a) 219 | b = np.dot(n[:2], point[:2]) 220 | return (a - b) / n[2] 221 | 222 | # Vertical plane. Use max edge elevation 223 | zs = [] 224 | a, b, c = p[s[0]], p[s[1]], p[s[2]] 225 | 226 | for v, w in ((a, b), (a, c), (b, c)): 227 | dv = np.linalg.norm(point[:2] - v[:2]) 228 | dvw = np.linalg.norm(w[:2] - v[:2]) 229 | z = (w[2] - v[2]) * dv / dvw + v[2] 230 | zs.append(z) 231 | 232 | return max(zs) 233 | 234 | def barycentric_coords(self, point, simplex): 235 | """ 236 | Returns the local barycentric coordinates for the input point. 237 | If any of the coordinates are less than 0, then the projection 238 | of the point onto the plane of the triangle is outside of the triangle. 239 | 240 | Parameters 241 | ---------- 242 | point : array 243 | A point for which barycentric coordinates will be calculated. 244 | The point must be of shape (3,). 245 | simplex : int 246 | The index of the simplex. 247 | """ 248 | point = np.asarray(point) 249 | 250 | p = self.points 251 | n = self.normal(simplex) 252 | s = self.tri.simplices[simplex] 253 | 254 | a, b, c = p[s[0]], p[s[1]], p[s[2]] 255 | 256 | u = b - a 257 | u = u / np.linalg.norm(u) 258 | v = np.cross(u, n) 259 | 260 | x1, y1 = np.dot(a, u), np.dot(a, v) 261 | x2, y2 = np.dot(b, u), np.dot(b, v) 262 | x3, y3 = np.dot(c, u), np.dot(c, v) 263 | 264 | det = (y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3) 265 | x1, y1, x2, y2 = y2 - y3, x3 - x2, y3 - y1, x1 - x3 266 | 267 | x, y = np.dot(point, u), np.dot(point, v) 268 | l1 = (x1*(x - x3) + y1*(y - y3)) / det 269 | l2 = (x2*(x - x3) + y2*(y - y3)) / det 270 | l3 = 1 - l1 - l2 271 | 272 | return np.array([l1, l2, l3]) 273 | 274 | def query_distances(self, point, radius): 275 | """ 276 | Finds the closest distances to all simplices within the specified 277 | xy-plane radius. 278 | 279 | Parameters 280 | ---------- 281 | point : array 282 | An array of shape (3,). 283 | radius : float 284 | The radius within the xy-plane in which simplices will be queried. 285 | 286 | Returns 287 | ------- 288 | distances : array 289 | An array of distances to simplices of shape (N,). 290 | tin_points : array 291 | An array of closest simplex points of shape (N, 3). 292 | """ 293 | point = np.asarray(point) 294 | simplices = self.query_simplices(point, radius) 295 | 296 | p = self.points 297 | s = self.tri.simplices[simplices] 298 | bary = [self.barycentric_coords(point, x) for x in simplices] 299 | bary = np.min(bary, axis=1) 300 | 301 | tin = np.zeros((simplices.shape[0], 3), dtype='float') 302 | dist = np.zeros(simplices.shape[0], dtype='float') 303 | 304 | for i, x in enumerate(bary): 305 | if x >= 0: 306 | # Plane distance 307 | n = self.normal(simplices[i]) 308 | d = np.dot(p[s[i, 0]] - point, n) 309 | tin[i] = d * n + point 310 | dist[i] = abs(d) 311 | continue 312 | 313 | # Edge distance 314 | a, b, c = p[s[i]] 315 | dist[i] = float('inf') 316 | 317 | for v, w in ((a, b), (b, c), (a, c)): 318 | u = w - v 319 | m = np.linalg.norm(u) 320 | u = u / m 321 | proj = np.dot(point - v, u) 322 | 323 | if proj <= 0: 324 | r = v 325 | elif proj >= m: 326 | r = w 327 | else: 328 | r = proj * u + v 329 | 330 | d = np.linalg.norm(r - point) 331 | 332 | if d < dist[i]: 333 | tin[i], dist[i] = r, d 334 | 335 | f = dist.argsort() 336 | return dist[f], tin[f] 337 | 338 | def plot_surface_3d(self, ax=None, cmap='terrain'): 339 | """ 340 | Plots a the rendered TIN surface in 3D 341 | 342 | Parameters 343 | ---------- 344 | ax : :class:`matplotlib.axes.Axes` 345 | The axes to which the plot will be added. If None, a new figure 346 | and axes will be created. 347 | cmap : str 348 | The name of the color map to use. 349 | 350 | Examples 351 | -------- 352 | .. plot:: ../examples/survey/tin_ex1.py 353 | :include-source: 354 | """ 355 | if ax is None: 356 | mx = self.points.max(axis=0) 357 | c = 0.5 * (mx + self.points.min(axis=0)) 358 | r = 1.1 * np.max(mx - c) 359 | xlim, ylim, zlim = np.column_stack([c - r, c + r]) 360 | 361 | fig = plt.figure() 362 | ax = fig.add_subplot(111, 363 | title=self.name, 364 | projection='3d', 365 | xlim=xlim, 366 | ylim=ylim, 367 | zlim=zlim, 368 | aspect='equal' 369 | ) 370 | 371 | x = self.points 372 | ax.plot_trisurf(x[:,0], x[:,1], x[:,2], 373 | triangles=self.tri.simplices, 374 | cmap=cmap 375 | ) 376 | 377 | return ax 378 | 379 | def plot_surface_2d(self, ax=None): 380 | """ 381 | Plots a the triangulation in 2D. 382 | 383 | Parameters 384 | ---------- 385 | ax : :class:`matplotlib.axes.Axes` 386 | The axes to which the plot will be added. If None, a new figure 387 | and axes will be created. 388 | 389 | Examples 390 | -------- 391 | .. plot:: ../examples/survey/tin_ex3.py 392 | :include-source: 393 | """ 394 | if ax is None: 395 | mx = self.points[:,:2].max(axis=0) 396 | c = 0.5 * (mx + self.points[:,:2].min(axis=0)) 397 | r = 1.1 * np.max(mx - c) 398 | xlim, ylim = np.column_stack([c - r, c + r]) 399 | 400 | fig = plt.figure() 401 | ax = fig.add_subplot(111, 402 | title=self.name, 403 | xlim=xlim, 404 | ylim=ylim, 405 | aspect='equal' 406 | ) 407 | 408 | x = self.points 409 | ax.triplot(x[:,0], x[:,1], triangles=self.tri.simplices) 410 | 411 | return ax 412 | 413 | def plot_contour_2d(self, ax=None, cmap='terrain'): 414 | """ 415 | Plots a the rendered TIN surface in 3D 416 | 417 | Parameters 418 | ---------- 419 | ax : :class:`matplotlib.axes.Axes` 420 | The axes to which the plot will be added. If None, a new figure 421 | and axes will be created. 422 | cmap : str 423 | The name of the color map to use. 424 | 425 | Examples 426 | -------- 427 | .. plot:: ../examples/survey/tin_ex2.py 428 | :include-source: 429 | """ 430 | if ax is None: 431 | mx = self.points[:,:2].max(axis=0) 432 | c = 0.5 * (mx + self.points[:,:2].min(axis=0)) 433 | r = 1.1 * np.max(mx - c) 434 | xlim, ylim = np.column_stack([c - r, c + r]) 435 | 436 | fig = plt.figure() 437 | ax = fig.add_subplot(111, 438 | title=self.name, 439 | xlim=xlim, 440 | ylim=ylim, 441 | aspect='equal' 442 | ) 443 | 444 | x = self.points 445 | contourf = ax.tricontourf(x[:,0], x[:,1], x[:,2], cmap=cmap) 446 | contour = ax.tricontour(x[:,0], x[:,1], x[:,2], colors='black') 447 | 448 | ax.clabel(contour, inline=True, fontsize=6) 449 | fig.colorbar(contourf) 450 | 451 | return ax 452 | -------------------------------------------------------------------------------- /civpy/survey/tin_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from .tin import * 4 | 5 | 6 | def TIN1(): 7 | p = np.array([ 8 | [-0.5, -0.5, 0], 9 | [ 0.5, -0.5, 0], 10 | [ 0.5, 0.5, 0], 11 | [-0.5, 0.5, 0], 12 | [ 0, 0, 0.5] 13 | ]) 14 | 15 | return TIN('TIN1', p) 16 | 17 | 18 | def TIN2(): 19 | p = np.array([ 20 | [-0.5, -0.5, 0], 21 | [ 0.5, -0.5, 0], 22 | [ 0.5, 0.5, 0], 23 | [-0.5, 0.5, 0], 24 | [ 0, 0, 0.5] 25 | ]) 26 | 27 | return TIN('TIN2', p, max_edge=0.1) 28 | 29 | 30 | def TIN3(): 31 | p = np.array([ 32 | [-0.5, -0.5, 0], 33 | [ 0.5, -0.5, 0], 34 | [ 0.5, 0.5, 0], 35 | [-0.5, 0.5, 0], 36 | [ 0, 0, 0.5] 37 | ]) 38 | 39 | # Breaklines 40 | b = np.linspace(0, 2*np.pi, 10) 41 | b = 0.5 * np.column_stack([np.cos(b), np.sin(b), np.zeros(len(b))]) 42 | 43 | return TIN('TIN3', p, [b], step=100) 44 | 45 | 46 | def test_repr(): 47 | t = TIN1() 48 | repr(t) 49 | 50 | 51 | def test_elevation(): 52 | t = TIN1() 53 | p = [(0, 0, 1), (0, 1, 0), (0, 0.5, 0.5), (0, 0.25, 0.25)] 54 | zs = [t.elevation(x) for x in p] 55 | np.testing.assert_equal(zs, [0.5, np.nan, 0, 0.25]) 56 | 57 | 58 | def test_remove_simplices(): 59 | t = TIN2() 60 | assert t.tri.simplices.shape[0] == 0 61 | 62 | 63 | def test_breakpoints(): 64 | t = TIN3() 65 | b = np.array(sorted(map(tuple, t.breaklines[0]))) 66 | bp = np.array(sorted(map(tuple, t.breakpoints()))) 67 | 68 | assert pytest.approx(bp) == b 69 | 70 | 71 | def test_query_distances(): 72 | t = TIN1() 73 | p = [(0, 0, 1), (0, 1, 0), (0, 0.5, 0.5), (0, 0.25, 0.25)] 74 | dist = [] 75 | tin = [] 76 | 77 | for x in p: 78 | a, b = t.query_distances(x, 5) 79 | dist.append(a[0]) 80 | tin.append(b[0]) 81 | 82 | dist = np.array(dist) 83 | tin = np.array(tin) 84 | 85 | a = np.array([0.5, 0.5, 0.353553391, 0]) 86 | b = np.array([(0, 0, 0.5), (0, 0.5, 0), (0, 0.25, 0.25), (0, 0.25, 0.25)]) 87 | 88 | assert pytest.approx(dist) == a 89 | assert pytest.approx(tin) == b 90 | 91 | 92 | def test_plot_surface_3d(): 93 | t = TIN1() 94 | t.plot_surface_3d() 95 | 96 | 97 | def test_plot_surface_2d(): 98 | t = TIN1() 99 | t.plot_surface_2d() 100 | 101 | 102 | def test_plot_contour_2d(): 103 | t = TIN1() 104 | t.plot_contour_2d() 105 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = civpy 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/alignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpewsey/civpy/bbf74b1c04ca9f7604831f5280cc80d796240e67/docs/_static/alignment.png -------------------------------------------------------------------------------- /docs/_static/spatial_hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpewsey/civpy/bbf74b1c04ca9f7604831f5280cc80d796240e67/docs/_static/spatial_hash.png -------------------------------------------------------------------------------- /docs/_static/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpewsey/civpy/bbf74b1c04ca9f7604831f5280cc80d796240e67/docs/_static/structure.png -------------------------------------------------------------------------------- /docs/_static/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpewsey/civpy/bbf74b1c04ca9f7604831f5280cc80d796240e67/docs/_static/summary.png -------------------------------------------------------------------------------- /docs/_static/tin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpewsey/civpy/bbf74b1c04ca9f7604831f5280cc80d796240e67/docs/_static/tin.png -------------------------------------------------------------------------------- /docs/_static/truss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpewsey/civpy/bbf74b1c04ca9f7604831f5280cc80d796240e67/docs/_static/truss.png -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | 7 | {% block methods %} 8 | {% if methods %} 9 | .. rubric:: Methods 10 | 11 | .. autosummary:: 12 | :toctree: 13 | {% for item in methods %} 14 | {% if item not in ['__init__', 'clear', 'fromkeys', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values', 'getfield', 'partition', 'all', 'any', 'argmax', 'argmin', 'argpartition', 'argsort', 'astype', 'byteswap', 'choose', 'clip', 'compress', 'conj', 'conjugate', 'cumprod', 'cumsum', 'diagonal', 'dot', 'dump', 'dumps', 'fill', 'flatten', 'item', 'itemset', 'max', 'mean', 'min', 'newbyteorder', 'nonzero', 'portition', 'prod', 'ptp', 'put', 'ravel', 'repeat', 'reshape', 'resize', 'round', 'searchsorted', 'setfield', 'setflags', 'sort', 'squeeze', 'std', 'sum', 'swapaxes', 'take', 'tobytes', 'tofile', 'tolist', 'tostring', 'trace', 'transpose', 'var', 'view'] %}~{{ name }}.{{ item }}{% endif %} 15 | {%- endfor %} 16 | {% endif %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /docs/_templates/searchbox.html: -------------------------------------------------------------------------------- 1 | {%- if pagename != "search" and builder != "singlehtml" %} 2 | 13 | 14 | {%- endif %} 15 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | import os 20 | import sys 21 | import shutil 22 | from configparser import ConfigParser 23 | 24 | root = os.path.abspath(os.path.join('..')) 25 | sys.path.insert(0, root) 26 | 27 | # Load metadata from setup.cfg 28 | meta_path = os.path.join(root, 'setup.cfg') 29 | meta = ConfigParser() 30 | meta.read(meta_path) 31 | meta = meta['metadata'] 32 | 33 | # Clear generated documents 34 | if (not os.path.exists('_build') or len(os.listdir('_build')) == 0) and os.path.exists('generated'): 35 | shutil.rmtree('generated') 36 | 37 | # -- Project information ----------------------------------------------------- 38 | 39 | project = meta['project'] 40 | copyright = meta['copyright'] 41 | author = meta['author'] 42 | 43 | # The short X.Y version 44 | version = meta['version'] 45 | # The full version, including alpha/beta/rc tags 46 | release = meta['version'] 47 | 48 | # -- General configuration --------------------------------------------------- 49 | 50 | # If your documentation needs a minimal Sphinx version, state it here. 51 | # 52 | # needs_sphinx = '1.0' 53 | 54 | # Add any Sphinx extension module names here, as strings. They can be 55 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 56 | # ones. 57 | extensions = [ 58 | 'matplotlib.sphinxext.plot_directive', 59 | 'sphinx.ext.autodoc', 60 | 'sphinx.ext.doctest', 61 | 'sphinx.ext.intersphinx', 62 | 'sphinx.ext.todo', 63 | 'sphinx.ext.coverage', 64 | 'sphinx.ext.mathjax', 65 | #'sphinx.ext.linkcode', 66 | 'numpydoc', 67 | #'sphinx.ext.imgmath', 68 | 'sphinx.ext.viewcode', 69 | #'sphinxcontrib.spelling', 70 | ] 71 | 72 | # Add any paths that contain templates here, relative to this directory. 73 | templates_path = ['_templates'] 74 | 75 | # The suffix(es) of source filenames. 76 | # You can specify multiple suffix as a list of string: 77 | # 78 | source_suffix = ['.rst', '.md'] 79 | #source_suffix = '.rst' 80 | 81 | # The master toctree document. 82 | master_doc = 'index' 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | # 87 | # This is also used if you do content translation via gettext catalogs. 88 | # Usually you set "language" from the command line for these cases. 89 | language = None 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | # This pattern also affects html_static_path and html_extra_path . 94 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'scipy-sphinx-theme/*'] 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | 100 | # -- Options for HTML output ------------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | # 105 | themedir = os.path.join(os.path.dirname(__file__), 'scipy-sphinx-theme', '_theme') 106 | 107 | if not os.path.isdir(themedir): 108 | raise RuntimeError('Get the scipy-sphinx-theme first, ' 109 | 'via git submodule init && git submodule update') 110 | 111 | 112 | html_theme = 'scipy' 113 | html_theme_path = [themedir] 114 | 115 | # numpydoc settings 116 | numpydoc_show_class_members = False 117 | numpydoc_show_inherited_class_members = False 118 | class_members_toctree = False 119 | autosummary_generate = True 120 | 121 | html_theme_options = { 122 | "edit_link": False, 123 | "sidebar": "left", 124 | "scipy_org_logo": False, 125 | "rootlinks": [] 126 | } 127 | 128 | html_sidebars = {'index': ['searchbox.html']} 129 | 130 | html_title = "%s v%s Manual" % (project, version) 131 | html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # Theme options are theme-specific and customize the look and feel of a theme 134 | # further. For a list of options available for each theme, see the 135 | # documentation. 136 | # 137 | # html_theme_options = {} 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Custom sidebar templates, must be a dictionary that maps document names 145 | # to template names. 146 | # 147 | # The default sidebars (for documents that don't match any pattern) are 148 | # defined by theme itself. Builtin themes are using these templates by 149 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 150 | # 'searchbox.html']``. 151 | # 152 | # html_sidebars = {} 153 | 154 | 155 | # -- Options for HTMLHelp output --------------------------------------------- 156 | 157 | # Output file base name for HTML help builder. 158 | htmlhelp_basename = '{}'.format(meta['name']) 159 | 160 | 161 | # -- Options for LaTeX output ------------------------------------------------ 162 | 163 | latex_elements = { 164 | 'classoptions': ',openany,oneside' 165 | 166 | # The paper size ('letterpaper' or 'a4paper'). 167 | # 168 | # 'papersize': 'letterpaper', 169 | 170 | # The font size ('10pt', '11pt' or '12pt'). 171 | # 172 | # 'pointsize': '10pt', 173 | 174 | # Additional stuff for the LaTeX preamble. 175 | # 176 | # 'preamble': '', 177 | 178 | # Latex figure (float) alignment 179 | # 180 | # 'figure_align': 'htbp', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, 185 | # author, documentclass [howto, manual, or own class]). 186 | latex_documents = [ 187 | (master_doc, '{}.tex'.format(meta['name']), '{} Documentation'.format(meta['project']), 188 | author, 'manual'), 189 | ] 190 | 191 | 192 | # -- Options for manual page output ------------------------------------------ 193 | 194 | # One entry per manual page. List of tuples 195 | # (source start file, name, description, authors, manual section). 196 | man_pages = [ 197 | (master_doc, meta['name'], '{} Documentation'.format(meta['project']), 198 | [author], 1) 199 | ] 200 | 201 | 202 | # -- Options for Texinfo output ---------------------------------------------- 203 | 204 | # Grouping the document tree into Texinfo files. List of tuples 205 | # (source start file, target name, title, author, 206 | # dir menu entry, description, category) 207 | texinfo_documents = [ 208 | (master_doc, meta['name'], '{} Documentation'.format(meta['project']), 209 | author, meta['name'], meta['description'], 210 | 'Miscellaneous'), 211 | ] 212 | 213 | 214 | # -- Extension configuration ------------------------------------------------- 215 | 216 | # -- Options for intersphinx extension --------------------------------------- 217 | 218 | # Example configuration for intersphinx: refer to the Python standard library. 219 | intersphinx_mapping = { 220 | 'python': ('https://docs.python.org/3', None), 221 | 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 222 | 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), 223 | 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None), 224 | } 225 | 226 | # -- Options for todo extension ---------------------------------------------- 227 | 228 | # If true, `todo` and `todoList` produce output, else they produce nothing. 229 | todo_include_todos = True 230 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | CivPy 3 | ===== 4 | 5 | .. image:: https://travis-ci.com/mpewsey/civpy.svg?branch=master 6 | :target: https://travis-ci.com/mpewsey/civpy 7 | 8 | .. image:: https://readthedocs.org/projects/civpy/badge/?version=latest 9 | :target: https://civpy.readthedocs.io/en/latest/?badge=latest 10 | 11 | .. image:: https://codecov.io/gh/mpewsey/civpy/branch/master/graph/badge.svg?token=zbJbsGGSoL 12 | :target: https://codecov.io/gh/mpewsey/civpy 13 | 14 | About 15 | ===== 16 | This package provides civil engineering tools and algorithms for creating 17 | survey and structure models in Python. 18 | 19 | 20 | Installation 21 | ============ 22 | The development version of this package may be installed via pip: 23 | 24 | .. code-block:: python 25 | 26 | pip install git+https://github.com/mpewsey/civpy#egg=civpy 27 | 28 | 29 | Table of Contents 30 | ================= 31 | 32 | .. toctree:: 33 | :maxdepth: 2 34 | 35 | math 36 | structures 37 | survey 38 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=civpy 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/math.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: civpy.math 2 | :members: 3 | -------------------------------------------------------------------------------- /docs/releases/.holder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpewsey/civpy/bbf74b1c04ca9f7604831f5280cc80d796240e67/docs/releases/.holder -------------------------------------------------------------------------------- /docs/structures.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: civpy.structures 2 | :members: 3 | -------------------------------------------------------------------------------- /docs/survey.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: civpy.survey 2 | :members: 3 | -------------------------------------------------------------------------------- /examples/structures/structure_ex1.py: -------------------------------------------------------------------------------- 1 | # structure_ex1.py 2 | import numpy as np 3 | from civpy.structures import * 4 | 5 | section = CrossSection( 6 | name='dummy', 7 | area=32.9, 8 | inertia_x=236, 9 | inertia_y=716, 10 | inertia_j=15.1 11 | ) 12 | 13 | material = Material( 14 | name='dummy', 15 | elasticity=29000, 16 | rigidity=11500 17 | ) 18 | 19 | group = ElementGroup( 20 | name='dummy', 21 | section=section, 22 | material=material 23 | ) 24 | 25 | nodes = [ 26 | Node('1', 0, 0, 0), 27 | Node('2', -240, 0, 0).fixed(), 28 | Node('3', 0, -240, 0).fixed(), 29 | Node('4', 0, 0, -240).fixed(), 30 | ] 31 | 32 | elements =[ 33 | Element('1', '2', '1', group), 34 | Element('2', '3', '1', group, roll=np.deg2rad(-90)), 35 | Element('3', '4', '1', group, roll=np.deg2rad(-30)), 36 | ] 37 | 38 | nloads = [ 39 | NodeLoad('1', mx=-1800, mz=1800) 40 | ] 41 | 42 | eloads = [ 43 | ElementLoad('1', fy=-0.25) 44 | ] 45 | 46 | lc = LoadCase('1', nloads, eloads) 47 | 48 | struct = Structure( 49 | name='dummy', 50 | nodes=nodes, 51 | elements=elements 52 | ) 53 | 54 | struct.plot_3d() 55 | result = struct.linear_analysis(lc) 56 | 57 | result['glob'] 58 | # load_case node force_x force_y force_z moment_x moment_y \ 59 | # 0 1 1 0.000000 0.000000 0.000000 -1800.000000 0.000000 60 | # 1 1 2 5.375736 44.106293 -0.742724 2.172151 58.987351 61 | # 2 1 3 -4.624913 11.117379 -6.460651 -515.545730 -0.764719 62 | # 3 1 4 -0.750823 4.776328 7.203376 -383.501559 -60.166419 63 | 64 | # moment_z defl_x defl_y defl_z rot_x rot_y rot_z 65 | # 0 1800.000000 -0.001352 -0.002797 -0.001812 -0.003002 0.001057 0.006499 66 | # 1 2330.519663 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 67 | # 2 369.671654 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 68 | # 3 -4.701994 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 69 | -------------------------------------------------------------------------------- /examples/structures/structure_ex2.py: -------------------------------------------------------------------------------- 1 | # structure_ex2.py 2 | import numpy as np 3 | from civpy.structures import * 4 | 5 | section = CrossSection( 6 | name='dummy', 7 | area=32.9, 8 | inertia_x=236, 9 | inertia_y=716, 10 | inertia_j=15.1 11 | ) 12 | 13 | material = Material( 14 | name='dummy', 15 | elasticity=29000, 16 | rigidity=11500 17 | ) 18 | 19 | group = ElementGroup( 20 | name='dummy', 21 | section=section, 22 | material=material 23 | ) 24 | 25 | nodes = [ 26 | Node('1', 20, 20, 0, 'xy').fixed(), 27 | Node('2', 15, 15, 50, 'xy') 28 | ] 29 | 30 | elements =[ 31 | Element('1', '1_p', '1_x', group, 'y'), 32 | Element('2', '1_p', '1_y', group, 'x'), 33 | Element('3', '2_p', '2_x', group, 'y'), 34 | Element('4', '2_p', '2_y', group, 'x'), 35 | Element('5', '1_p', '2_p', group, 'xy'), 36 | Element('6', '1_p', '2_x', group, 'xy'), 37 | Element('7', '1_x', '2_xy', group, 'xy') 38 | ] 39 | 40 | struct = Structure( 41 | name='dummy', 42 | nodes=nodes, 43 | elements=elements, 44 | symmetry=True 45 | ) 46 | 47 | struct.plot_3d() 48 | -------------------------------------------------------------------------------- /examples/structures/structure_ex3.py: -------------------------------------------------------------------------------- 1 | # structure_ex3.py 2 | import numpy as np 3 | from civpy.structures import * 4 | 5 | section = CrossSection( 6 | name='dummy', 7 | area=1 8 | ) 9 | 10 | material = Material( 11 | name='dummy', 12 | elasticity=29000 13 | ) 14 | 15 | group = ElementGroup( 16 | name='dummy', 17 | section=section, 18 | material=material 19 | ) 20 | 21 | nodes = [ 22 | Node('1', 120, 120).fixed(), 23 | Node('2', 120, 0, fx_free=False), 24 | Node('3', 0, 0), 25 | Node('4', 0, 120) 26 | ] 27 | 28 | elements =[ 29 | Element('1', '1', '2', group), 30 | Element('2', '3', '2', group), 31 | Element('3', '3', '4', group), 32 | Element('4', '4', '1', group), 33 | Element('5', '1', '3', group), 34 | Element('6', '4', '2', group) 35 | ] 36 | 37 | struct = Structure( 38 | name='dummy', 39 | nodes=nodes, 40 | elements=elements 41 | ) 42 | 43 | struct.plot_2d() 44 | -------------------------------------------------------------------------------- /examples/survey/alignment_ex1.py: -------------------------------------------------------------------------------- 1 | # alignment_ex1.py 2 | from civpy.survey import PI, Alignment, SurveyStake 3 | 4 | # PI coordinates 5 | p = [ 6 | # x, y z, r 7 | (-100, -200, 0, 0), 8 | (-200, -200, 0, 40), 9 | (-200, 200, 0, 40), 10 | ( 200, 200, 0, 40), 11 | ( 200, -200, 0, 40), 12 | ( 100, -200, 0, 40), 13 | ( 100, 100, 0, 40), 14 | (-100, 100, 0, 40), 15 | (-100, -100, 0, 40), 16 | ( 0, -100, 0, 40), 17 | ( 0, 0, 0, 0) 18 | ] 19 | 20 | # Stake survey stations 21 | q = [ 22 | # sta, off, z, ht, rot, 23 | ( 0, 30, 0, 0, -0.78539), 24 | ( 100, 30, 0, 0, 0), 25 | ( 300, -30, 0, 0, 0), 26 | ( 475, -30, 0, 0, 0), 27 | (1000, 0, 0, 0, 0), 28 | (1975, -30, 0, 0, 0) 29 | ] 30 | 31 | p = [PI(*x) for x in p] 32 | q = [SurveyStake.init_station(*x) for x in q] 33 | 34 | align = Alignment('Alignment', pis=p, stakes=q) 35 | align.plot_plan() 36 | -------------------------------------------------------------------------------- /examples/survey/spatial_hash_ex1.py: -------------------------------------------------------------------------------- 1 | # spatial_hash_ex1.py 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | from civpy.survey import SpatialHash 5 | 6 | fig = plt.figure(figsize=(8, 8)) 7 | 8 | # 1D Spatial Hash 9 | np.random.seed(138793874) 10 | ax1 = fig.add_subplot(221, 11 | xlim=(-400, 300) 12 | ) 13 | x = np.random.normal(0, 100, (50, 1)) 14 | s = SpatialHash(x, 100) 15 | ax = s.plot(ax=ax1) 16 | ax.set_title('1D Spatial Hash') 17 | 18 | # 2D Spatial Hash 19 | np.random.seed(53287442) 20 | ax2 = fig.add_subplot(222, 21 | xlim=(-500, 400), 22 | ylim=(-500, 400), 23 | aspect='equal' 24 | ) 25 | x = np.random.normal(0, 100, (200, 2)) 26 | s = SpatialHash(x, 100) 27 | ax = s.plot(ax=ax2) 28 | ax.set_title('2D Spatial Hash') 29 | 30 | # 3D Spatial Hash 31 | np.random.seed(8973489) 32 | ax3 = fig.add_subplot(223, 33 | projection='3d', 34 | xlim=(-10, 90), 35 | ylim=(-10, 90), 36 | zlim=(-10, 90), 37 | aspect='equal' 38 | ) 39 | x = np.random.uniform(0, 80, (25, 3)) 40 | s = SpatialHash(x, 10) 41 | ax = s.plot(ax=ax3) 42 | ax.set_title('3D Spatial Hash') 43 | -------------------------------------------------------------------------------- /examples/survey/spatial_hash_ex2.py: -------------------------------------------------------------------------------- 1 | # spatial_hash_ex2.py 2 | import numpy as np 3 | from civpy.survey import SpatialHash 4 | from matplotlib.patches import Rectangle, Circle 5 | 6 | r = 30 # Query radius 7 | np.random.seed(32874393) 8 | x = np.random.uniform(-60, 60, (30000, 2)) 9 | s = SpatialHash(x, 10) 10 | ax = s.plot(symbols=dict(points='r,')) 11 | 12 | # Find points within radius of points 13 | points = np.array([ 14 | (30, -45), 15 | (60, 20) 16 | ]) 17 | 18 | # Plot query points 19 | ax.plot(points[:,0], points[:,1], 'ko', zorder=10) 20 | 21 | for p in points: 22 | # Plot found points 23 | i = s.query_point(p, r) 24 | xp = s.points[i] 25 | ax.plot(xp[:,0], xp[:,1], 'g,', zorder=5) 26 | 27 | # Plot circle 28 | circ = Circle(p, r, color='k', fill=False, zorder=10) 29 | ax.add_artist(circ) 30 | 31 | 32 | # Find points within offset of range 33 | ranges = np.array([ 34 | [(-50, -50), (0, 60)] 35 | ]) 36 | 37 | for a, b in ranges: 38 | # Plot found points 39 | i = s.query_range(a, b, r) 40 | xp = s.points[i] 41 | ax.plot(xp[:,0], xp[:,1], 'g,') 42 | 43 | # Plot rectangles 44 | dx, dy = b - a 45 | c = np.linalg.norm(b - a) 46 | ang = np.arctan2(dy, dx) * 180/np.pi 47 | 48 | rect = Rectangle(a, c, r, angle=ang, color='k', fill=False, zorder=10) 49 | ax.add_artist(rect) 50 | 51 | rect = Rectangle(b, c, r, angle=ang+180, color='k', fill=False, zorder=10) 52 | ax.add_artist(rect) 53 | -------------------------------------------------------------------------------- /examples/survey/tin_ex1.py: -------------------------------------------------------------------------------- 1 | # tin_ex1.py 2 | import numpy as np 3 | from civpy.survey import TIN 4 | 5 | # TIN points 6 | p = np.array([ 7 | (-0.5, -0.5, 0), 8 | ( 0.5, -0.5, 0), 9 | ( 0.5, 0.5, 0), 10 | (-0.5, 0.5, 0), 11 | ( 0, 0, 0.5) 12 | ]) 13 | 14 | t = TIN('Distance Query', p) 15 | ax = t.plot_surface_3d() 16 | 17 | # Query points 18 | q = np.array([ 19 | ( 0, 0, 1), 20 | (0.5, 0, 0.5), 21 | ( 1, 0, 0) 22 | ]) 23 | 24 | ax.plot(q[:,0], q[:,1], q[:,2], 'r.') 25 | 26 | for i, x in enumerate(q): 27 | _, r = t.query_distances(x, 5) 28 | r = np.column_stack([x, r[0]]) 29 | ax.text(x[0], x[1], x[2], i) 30 | ax.plot(r[0], r[1], r[2], 'r-') 31 | -------------------------------------------------------------------------------- /examples/survey/tin_ex2.py: -------------------------------------------------------------------------------- 1 | # tin_ex2.py 2 | import numpy as np 3 | from civpy.survey import TIN 4 | 5 | # Generate grid coordinates 6 | x, y = [], [] 7 | 8 | for xi in np.linspace(2, 7, 50): 9 | for yi in np.linspace(0, 5, 50): 10 | x.append(xi) 11 | y.append(yi) 12 | 13 | x, y = np.array(x), np.array(y) 14 | z = np.sin(x) + np.cos(y*x - 5) * np.cos(x) + 2 15 | 16 | p = np.column_stack([x, y, z]) 17 | 18 | t = TIN('Contour', p, grid=10) 19 | 20 | t.plot_contour_2d() 21 | -------------------------------------------------------------------------------- /examples/survey/tin_ex3.py: -------------------------------------------------------------------------------- 1 | # tin_ex2.py 2 | import numpy as np 3 | from civpy.survey import TIN 4 | 5 | np.random.seed(32343) 6 | x = np.random.uniform(0, 60, 100) 7 | y = np.random.uniform(0, 60, 100) 8 | z = np.random.uniform(0, 20, 100) 9 | 10 | p = np.column_stack([x, y, z]) 11 | 12 | t = TIN('Surface', p, grid=10) 13 | 14 | t.plot_surface_2d() 15 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = tests.py test_*.py *_test.py 3 | addopts = --cov=. --cov-report=html 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.14.5 2 | scipy>=1.1.0 3 | matplotlib>=2.2.2 4 | attrs>=19.1.0 5 | pandas>=0.23.0 6 | xsect>=1.1.0 7 | 8 | # test 9 | pytest>=3.5.1 10 | pytest-cov>=2.5.1 11 | codecov>=2.0.15 12 | 13 | # docs 14 | docutils==0.14 15 | numpydoc>=0.8.0 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | project = CivPy 6 | name = civpy 7 | version = 0.0.1 8 | author = Matt Pewsey 9 | copyright = 2019, Matt Pewsey 10 | description = Civil engineering design tools and algorithms. 11 | long_description = file: README.md 12 | long_description_content_type = text/markdown 13 | license = BSD 3-Clause License 14 | license_file = LICENSE 15 | url = https://github.com/mpewsey/civpy 16 | keywords = 17 | civil-engineering 18 | engineering 19 | classifiers = 20 | Programming Language :: Python :: 3.5 21 | Programming Language :: Python :: 3.6 22 | Operating System :: OS Independent 23 | License :: OSI Approved :: BSD License 24 | 25 | [options] 26 | packages = find: 27 | include_package_data = True 28 | install_requires = 29 | numpy>=1.14.5 30 | scipy>=1.1.0 31 | matplotlib>=2.2.2 32 | attrs>=19.1.0 33 | pandas>=0.23.0 34 | xsect>=1.1.0 35 | python_requires = >=3.5 36 | 37 | [options.extras_require] 38 | test = 39 | pytest>=3.5.1 40 | pytest-cov>=2.5.1 41 | codecov>=2.0.15 42 | docs = 43 | docutils==0.14 44 | numpydoc>=0.8.0 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from glob import glob 3 | from setuptools import setup 4 | from setuptools.config import read_configuration 5 | 6 | config = read_configuration('setup.cfg') 7 | config_dict = {} 8 | 9 | for section in config: 10 | for k in config[section]: 11 | config_dict[k] = config[section][k] 12 | 13 | if os.path.exists('scripts'): 14 | config_dict['scripts'] = glob(os.path.join('scripts', '*')) 15 | 16 | setup(**config_dict) 17 | --------------------------------------------------------------------------------