├── .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 | [](https://travis-ci.com/mpewsey/civpy)
4 | [](https://civpy.readthedocs.io/en/latest/?badge=latest)
5 | [](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 |
27 | 
28 | Find points in N-dimensional spaces using spatial hash distance queries.
29 | |
30 |
31 | 
32 | Create ground surface triangulations, find surface elevations, and
33 | perform point distance queries to the surface.
34 | |
35 |
36 |
37 |
38 | 
39 | Model survey alignments with or without horizontal curves and perform coordinate calculations.
40 | |
41 |
42 | |
43 |
44 |
45 |
46 | ### Structure Modeling
47 |
48 |
49 |
50 |
51 | 
52 | Perform analysis of 2D and 3D trusses and manipulate node, element, and reaction results using Pandas data frames.
53 | |
54 |
55 |
56 |
57 | 
58 | Model structures with symmetry.
59 | |
60 |
61 | 
62 | Plot structure geometry in 3D or in 2D cross sections.
63 | |
64 |
65 |
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 |
3 |
{{ _('Quick search') }}
4 |
5 |
11 |
12 |
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 |
--------------------------------------------------------------------------------