├── .bumpversion.cfg ├── .gitignore ├── .pylintrc ├── .pypirc.enc ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── conf.py ├── content │ └── basic.rst ├── index.rst └── make.bat ├── requirements.txt ├── requirements_dev.txt ├── setup.py ├── tests ├── test_docs.py ├── test_vector2.py └── test_vector3.py └── vectormath ├── __init__.py └── vector.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.2 3 | files = setup.py vectormath/__init__.py docs/conf.py README.rst 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | .pypirc 91 | pylint.html 92 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins= 20 | 21 | # Use multiple processes to speed up Pylint. 22 | jobs=1 23 | 24 | # Allow loading of arbitrary C extensions. Extensions are imported into the 25 | # active Python interpreter and may run arbitrary code. 26 | unsafe-load-any-extension=no 27 | 28 | # A comma-separated list of package or module names from where C extensions may 29 | # be loaded. Extensions are loading into the active Python interpreter and may 30 | # run arbitrary code 31 | extension-pkg-whitelist= 32 | 33 | # Allow optimization of some AST trees. This will activate a peephole AST 34 | # optimizer, which will apply various small optimizations. For instance, it can 35 | # be used to obtain the result of joining multiple strings with the addition 36 | # operator. Joining a lot of strings can lead to a maximum recursion error in 37 | # Pylint and this flag can prevent that. It has one side effect, the resulting 38 | # AST will be different than the one from reality. 39 | optimize-ast=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Enable the message, report, category or checker with the given id(s). You can 49 | # either give multiple identifier separated by comma (,) or put this option 50 | # multiple time (only on the command line, not in the configuration file where 51 | # it should appear only once). See also the "--disable" option for examples. 52 | #enable= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once).You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use"--disable=all --enable=classes 62 | # --disable=W" 63 | disable=E1111,E1121,W0223 64 | 65 | 66 | [REPORTS] 67 | 68 | # Set the output format. Available formats are text, parseable, colorized, msvs 69 | # (visual studio) and html. You can also give a reporter class, eg 70 | # mypackage.mymodule.MyReporterClass. 71 | output-format=text 72 | 73 | # Put messages in a separate file for each module / package specified on the 74 | # command line instead of printing them on stdout. Reports (if any) will be 75 | # written in a file name "pylint_global.[txt|html]". 76 | files-output=no 77 | 78 | # Tells whether to display a full report or only the messages 79 | reports=yes 80 | 81 | # Python expression which should return a note less than 10 (10 is the highest 82 | # note). You have access to the variables errors warning, statement which 83 | # respectively contain the number of errors / warnings messages and the total 84 | # number of statements analyzed. This is used by the global evaluation report 85 | # (RP0004). 86 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 87 | 88 | # Template used to display messages. This is a python new-style format string 89 | # used to format the message information. See doc for all details 90 | #msg-template= 91 | 92 | 93 | [BASIC] 94 | 95 | # List of builtins function names that should not be used, separated by a comma 96 | bad-functions=map,filter,input 97 | 98 | # Good variable names which should always be accepted, separated by a comma 99 | good-names=i,j,k,ex,Run,_,x,y,z,X,Y,Z,nV 100 | 101 | # Bad variable names which should always be refused, separated by a comma 102 | bad-names=foo,bar,baz,toto,tutu,tata 103 | 104 | # Colon-delimited sets of names that determine each other's naming style when 105 | # the name regexes allow several styles. 106 | name-group= 107 | 108 | # Include a hint for the correct naming format with invalid-name 109 | include-naming-hint=no 110 | 111 | # Regular expression matching correct function names 112 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 113 | 114 | # Naming hint for function names 115 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 116 | 117 | # Regular expression matching correct variable names 118 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 119 | 120 | # Naming hint for variable names 121 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 122 | 123 | # Regular expression matching correct constant names 124 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 125 | 126 | # Naming hint for constant names 127 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 128 | 129 | # Regular expression matching correct attribute names 130 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 131 | 132 | # Naming hint for attribute names 133 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 134 | 135 | # Regular expression matching correct argument names 136 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 137 | 138 | # Naming hint for argument names 139 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 140 | 141 | # Regular expression matching correct class attribute names 142 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 143 | 144 | # Naming hint for class attribute names 145 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 146 | 147 | # Regular expression matching correct inline iteration names 148 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 149 | 150 | # Naming hint for inline iteration names 151 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 152 | 153 | # Regular expression matching correct class names 154 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 155 | 156 | # Naming hint for class names 157 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 158 | 159 | # Regular expression matching correct module names 160 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 161 | 162 | # Naming hint for module names 163 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 164 | 165 | # Regular expression matching correct method names 166 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 167 | 168 | # Naming hint for method names 169 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 170 | 171 | # Regular expression which should only match function or class names that do 172 | # not require a docstring. 173 | no-docstring-rgx=^_ 174 | 175 | # Minimum line length for functions/classes that require docstrings, shorter 176 | # ones are exempt. 177 | docstring-min-length=-1 178 | 179 | 180 | [ELIF] 181 | 182 | # Maximum number of nested blocks for function / method body 183 | max-nested-blocks=5 184 | 185 | 186 | [FORMAT] 187 | 188 | # Maximum number of characters on a single line. 189 | max-line-length=100 190 | 191 | # Regexp for a line that is allowed to be longer than the limit. 192 | ignore-long-lines=^\s*(# )??$ 193 | 194 | # Allow the body of an if to be on the same line as the test if there is no 195 | # else. 196 | single-line-if-stmt=no 197 | 198 | # List of optional constructs for which whitespace checking is disabled. `dict- 199 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 200 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 201 | # `empty-line` allows space-only lines. 202 | no-space-check=trailing-comma,dict-separator 203 | 204 | # Maximum number of lines in a module 205 | max-module-lines=1000 206 | 207 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 208 | # tab). 209 | indent-string=' ' 210 | 211 | # Number of spaces of indent required inside a hanging or continued line. 212 | indent-after-paren=4 213 | 214 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 215 | expected-line-ending-format= 216 | 217 | 218 | [LOGGING] 219 | 220 | # Logging modules to check that the string format arguments are in logging 221 | # function parameter format 222 | logging-modules=logging 223 | 224 | 225 | [MISCELLANEOUS] 226 | 227 | # List of note tags to take in consideration, separated by a comma. 228 | notes=FIXME,XXX,TODO 229 | 230 | 231 | [SIMILARITIES] 232 | 233 | # Minimum lines number of a similarity. 234 | min-similarity-lines=20 235 | 236 | # Ignore comments when computing similarities. 237 | ignore-comments=yes 238 | 239 | # Ignore docstrings when computing similarities. 240 | ignore-docstrings=yes 241 | 242 | # Ignore imports when computing similarities. 243 | ignore-imports=yes 244 | 245 | 246 | [SPELLING] 247 | 248 | # Spelling dictionary name. Available dictionaries: none. To make it working 249 | # install python-enchant package. 250 | spelling-dict= 251 | 252 | # List of comma separated words that should not be checked. 253 | spelling-ignore-words= 254 | 255 | # A path to a file that contains private dictionary; one word per line. 256 | spelling-private-dict-file= 257 | 258 | # Tells whether to store unknown words to indicated private dictionary in 259 | # --spelling-private-dict-file option instead of raising a message. 260 | spelling-store-unknown-words=no 261 | 262 | 263 | [TYPECHECK] 264 | 265 | # Tells whether missing members accessed in mixin class should be ignored. A 266 | # mixin class is detected if its name ends with "mixin" (case insensitive). 267 | ignore-mixin-members=yes 268 | 269 | # List of module names for which member attributes should not be checked 270 | # (useful for modules/projects where namespaces are manipulated during runtime 271 | # and thus existing member attributes cannot be deduced by static analysis. It 272 | # supports qualified module names, as well as Unix pattern matching. 273 | ignored-modules= 274 | 275 | # List of classes names for which member attributes should not be checked 276 | # (useful for classes with attributes dynamically set). This supports can work 277 | # with qualified names. 278 | ignored-classes= 279 | 280 | # List of members which are set dynamically and missed by pylint inference 281 | # system, and so shouldn't trigger E1101 when accessed. Python regular 282 | # expressions are accepted. 283 | generated-members= 284 | 285 | 286 | [VARIABLES] 287 | 288 | # Tells whether we should check for unused import in __init__ files. 289 | init-import=no 290 | 291 | # A regular expression matching the name of dummy variables (i.e. expectedly 292 | # not used). 293 | dummy-variables-rgx=_$|dummy 294 | 295 | # List of additional names supposed to be defined in builtins. Remember that 296 | # you should avoid to define new builtins when possible. 297 | additional-builtins= 298 | 299 | # List of strings which can identify a callback function by name. A callback 300 | # name must start or end with one of those strings. 301 | callbacks=cb_,_cb 302 | 303 | 304 | [CLASSES] 305 | 306 | # List of method names used to declare (i.e. assign) instance attributes. 307 | defining-attr-methods=__init__,__new__,setUp 308 | 309 | # List of valid names for the first argument in a class method. 310 | valid-classmethod-first-arg=cls 311 | 312 | # List of valid names for the first argument in a metaclass class method. 313 | valid-metaclass-classmethod-first-arg=mcs 314 | 315 | # List of member names, which should be excluded from the protected access 316 | # warning. 317 | exclude-protected=_asdict,_fields,_replace,_source,_make,_props 318 | 319 | 320 | [DESIGN] 321 | 322 | # Maximum number of arguments for function / method 323 | max-args=5 324 | 325 | # Argument names that match this expression will be ignored. Default to name 326 | # with leading underscore 327 | ignored-argument-names=_.* 328 | 329 | # Maximum number of locals for function / method body 330 | max-locals=15 331 | 332 | # Maximum number of return / yield for function / method body 333 | max-returns=6 334 | 335 | # Maximum number of branch for function / method body 336 | max-branches=12 337 | 338 | # Maximum number of statements in function / method body 339 | max-statements=50 340 | 341 | # Maximum number of parents for a class (see R0901). 342 | max-parents=7 343 | 344 | # Maximum number of attributes for a class (see R0902). 345 | max-attributes=7 346 | 347 | # Minimum number of public methods for a class (see R0903). 348 | min-public-methods=2 349 | 350 | # Maximum number of public methods for a class (see R0904). 351 | max-public-methods=20 352 | 353 | # Maximum number of boolean expressions in a if statement 354 | max-bool-expr=5 355 | 356 | 357 | [IMPORTS] 358 | 359 | # Deprecated modules which should not be used, separated by a comma 360 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 361 | 362 | # Create a graph of every (i.e. internal and external) dependencies in the 363 | # given file (report RP0402 must not be disabled) 364 | import-graph= 365 | 366 | # Create a graph of external dependencies in the given file (report RP0402 must 367 | # not be disabled) 368 | ext-import-graph= 369 | 370 | # Create a graph of internal dependencies in the given file (report RP0402 must 371 | # not be disabled) 372 | int-import-graph= 373 | 374 | 375 | [EXCEPTIONS] 376 | 377 | # Exceptions that will emit a warning when being caught. Defaults to 378 | # "Exception" 379 | overgeneral-exceptions=Exception 380 | -------------------------------------------------------------------------------- /.pypirc.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seequent/vectormath/a2259fb82cf5a665170f50d216b11a738400d878/.pypirc.enc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7.13 4 | - 3.4 5 | - 3.5 6 | - 3.6 7 | 8 | sudo: false 9 | 10 | addons: 11 | apt: 12 | packages: 13 | - python-numpy 14 | 15 | install: 16 | - pip install -r requirements_dev.txt 17 | 18 | script: 19 | - make tests 20 | 21 | after_success: 22 | - bash <(curl -s https://codecov.io/bash) 23 | - openssl aes-256-cbc -K $encrypted_0f57b1eef12d_key -iv $encrypted_0f57b1eef12d_iv -in .pypirc.enc -out ~/.pypirc -d 24 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" -a $TRAVIS_PYTHON_VERSION == "2.7.13" -a -n "$(grep version setup.py | cut -d \' -f 2 | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$')" ]; then 25 | make publish; 26 | fi 27 | - if [ "$TRAVIS_BRANCH" = "beta" -a "$TRAVIS_PULL_REQUEST" = "false" -a $TRAVIS_PYTHON_VERSION == "2.7.13" -a -n "$(grep version setup.py | cut -d \' -f 2 | grep -E '^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$')" ]; then 28 | make publish; 29 | fi 30 | 31 | notifications: 32 | slack: 33 | secure: "C8P6i9K+OjBLPngi1R20OVL4Xe6xe5DCfUMR33ebHtMA/sfUnMuoRgZf5f+wjWsJNBBVC6kRFBETk4/SlWTwu0VrjFPsFjRqjX9fy/jOt2s5PD+hVGclx1fDd8t8FDsWJ9KMG15hKgoM2BFnOC9MOI7mvRt1V4PvskJ9rhqzLw3gdvwsAM0fFs12CE0lEQowieLKnIoBtWIV/ihfnorLid3led+b+vCKZcSLig3FR1fN56yS9qJXPw7lgUZX5qyyL1gHrV+VKX+fQpI/tDX611w6BLHlx0Y45pDH5jVGtMoQzhxQ6QH2ibrdw/3hHE9Ki7CS36h8oyzQ7hxXP6iwI16uPUqJRIqwdQcaHvNP7CnpnCyRtbhbOS0fCHjduXKWo+ttP++1x11vlj2F4qOg9tEWQH/6EU4rmo/naUwrTtybTUY0UmWWx8ASF4/kjZSYr9b0xZHwM3nVAwlfxNokvk/BPZs04/lm42UucZEC+IbVV4LTh1TURbXScDKj5Ve01nUfhrQkAqcl4TsQ1n2PAG+T//Ia3vO9r2w5kBSNaXA0Q8WDc51z/mdCmkFnQLrxZOMvRDQ8vbI8J7fMtPG/LmjmiXYK8bOf71pDwn3qYPFsf96Jg2wgAcJ2fPQt8apllCULzl0gZ0xOlC6zgEkCXCb/LLpBS+ud/C4t0Ureq/k=" 34 | email: false 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Seequent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME=vectormath 2 | 3 | .PHONY: install publish docs coverage lint lint-html graphs tests 4 | 5 | install: 6 | python setup.py install 7 | 8 | publish: 9 | python setup.py sdist upload 10 | 11 | docs: 12 | cd docs && make html 13 | 14 | coverage: 15 | nosetests --logging-level=INFO --with-coverage --cover-package=$(PACKAGE_NAME) --cover-html 16 | open cover/index.html 17 | 18 | lint: 19 | pylint $(PACKAGE_NAME) 20 | 21 | lint-html: 22 | pylint --output-format=html $(PACKAGE_NAME) > pylint.html 23 | 24 | graphs: 25 | pyreverse -my -A -o pdf -p vectormathpy $(PACKAGE_NAME)/**.py $(PACKAGE_NAME)/**/**.py 26 | 27 | tests: 28 | nosetests --logging-level=INFO --with-coverage --cover-package=$(PACKAGE_NAME) 29 | make lint 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | vectormath 2 | ========== 3 | 4 | 5 | .. image:: https://img.shields.io/pypi/v/vectormath.svg 6 | :target: https://pypi.org/project/vectormath 7 | :alt: Latest PyPI version 8 | 9 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg 10 | :target: https://github.com/seequent/vectormath/blob/master/LICENSE 11 | :alt: MIT license 12 | 13 | .. image:: https://api.travis-ci.org/seequent/vectormath.svg?branch=master 14 | :target: https://travis-ci.org/seequent/vectormath 15 | :alt: Travis CI build status 16 | 17 | .. image:: https://codecov.io/gh/seequent/vectormath/branch/master/graph/badge.svg 18 | :target: https://codecov.io/gh/seequent/vectormath 19 | :alt: Code test coverage 20 | 21 | 22 | Vector math utilities for Python built on `NumPy `_ 23 | 24 | 25 | Why 26 | --- 27 | 28 | The :code:`vectormath` package provides a fast, simple library of vector math 29 | utilities by leveraging NumPy. This allows explicit 30 | geometric constructs to be created (for example, :code:`Vector3` and :code:`Plane`) 31 | without redefining the underlying array math. 32 | 33 | Scope 34 | ----- 35 | 36 | The :code:`vectormath` package includes :code:`Vector3`/:code:`Vector2` and 37 | :code:`Vector3Array`/:code:`Vector2Array`. 38 | 39 | 40 | Goals 41 | ----- 42 | 43 | * Speed: All low-level operations rely on NumPy arrays. These are densely packed, 44 | typed, and partially implemented in C. The :code:`VectorArray` classes in particular 45 | take advantage of this speed by performing vector operations on all Vectors at 46 | once, rather than in a loop. 47 | * Simplicty: High-level operations are explicit and straight-forward. 48 | This library should be usable by Programmers, Mathematicians, and Geologists. 49 | 50 | 51 | Alternatives 52 | ------------ 53 | 54 | * `NumPy `_ can be used for any array operations 55 | * Many small libraries on PyPI (e.g. `vectors `_) 56 | implement vector math operations but are are only built with single vectors 57 | in mind. 58 | 59 | Connections 60 | ----------- 61 | 62 | * `properties `_ uses :code:`vectormath` 63 | as the underlying framework for Vector properties. 64 | 65 | Installation 66 | ------------ 67 | 68 | To install the repository, ensure that you have 69 | `pip installed `_ and run: 70 | 71 | .. code:: 72 | 73 | pip install vectormath 74 | 75 | For the development version: 76 | 77 | .. code:: 78 | 79 | git clone https://github.com/seequent/vectormath.git 80 | cd vectormath 81 | pip install -e . 82 | 83 | 84 | Examples 85 | ======== 86 | 87 | This example gives a brief demonstration of some of the notable features of 88 | :code:`Vector3` and :code:`Vector3Array` 89 | 90 | .. code:: python 91 | 92 | import numpy as np 93 | import vectormath as vmath 94 | 95 | # Single Vectors 96 | v = vmath.Vector3(5, 0, 0) 97 | v.normalize() 98 | print(v) # >> [1, 0, 0] 99 | print(v.x) # >> 1.0 100 | 101 | # VectorArrays are much faster than a for loop over Vectors 102 | v_array = vmath.Vector3Array([[4, 0, 0], [0, 2, 0], [0, 0, 3]]) 103 | print(v_array.x) # >> [4, 0, 0] 104 | print(v_array.length) # >> [4, 2, 3] 105 | print(v_array.normalize()) # >> [[1, 0, 0], [0, 1, 0], [0, 0, 1]] 106 | 107 | # Vectors can be accessed individually or in slices 108 | print(type(v_array[1:])) # >> vectormath.Vector3Array 109 | print(type(v_array[2])) # >> vectormath.Vector3 110 | 111 | # All these classes are just numpy arrays 112 | print(isinstance(v, np.ndarray)) # >> True 113 | print(type(v_array[1:, 1:])) # >> numpy.ndarray 114 | 115 | 116 | Current version: v0.2.2 117 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/vectormath.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/vectormath.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/vectormath" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/vectormath" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # vectormath documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Sep 26 14:50:08 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.doctest', 34 | 'sphinx.ext.intersphinx', 35 | 'sphinx.ext.mathjax', 36 | 'sphinx.ext.viewcode', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = u'vectormath' 55 | copyright = u'2018, Seequent' 56 | author = u'Seequent' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = u'0.2.2' 64 | # The full version, including alpha/beta/rc tags. 65 | release = u'0.2.2' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | # This patterns also effect to html_static_path and html_extra_path 83 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | #modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built documents. 107 | #keep_warnings = False 108 | 109 | # If true, `todo` and `todoList` produce output, else they produce nothing. 110 | todo_include_todos = False 111 | 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | # The theme to use for HTML and HTML Help pages. See the documentation for 116 | # a list of builtin themes. 117 | try: 118 | import sphinx_rtd_theme 119 | html_theme = 'sphinx_rtd_theme' 120 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 121 | pass 122 | except: 123 | html_theme = 'default' 124 | 125 | # Theme options are theme-specific and customize the look and feel of a theme 126 | # further. For a list of options available for each theme, see the 127 | # documentation. 128 | #html_theme_options = {} 129 | 130 | # Add any paths that contain custom themes here, relative to this directory. 131 | #html_theme_path = [] 132 | 133 | # The name for this set of Sphinx documents. 134 | # " v documentation" by default. 135 | #html_title = u'vectormath v0.2.2' 136 | 137 | # A shorter title for the navigation bar. Default is the same as html_title. 138 | #html_short_title = None 139 | 140 | # The name of an image file (relative to this directory) to place at the top 141 | # of the sidebar. 142 | #html_logo = None 143 | 144 | # The name of an image file (relative to this directory) to use as a favicon of 145 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 146 | # pixels large. 147 | #html_favicon = None 148 | 149 | # Add any paths that contain custom static files (such as style sheets) here, 150 | # relative to this directory. They are copied after the builtin static files, 151 | # so a file named "default.css" will overwrite the builtin "default.css". 152 | # html_static_path = ['_static'] 153 | 154 | # Add any extra paths that contain custom files (such as robots.txt or 155 | # .htaccess) here, relative to this directory. These files are copied 156 | # directly to the root of the documentation. 157 | #html_extra_path = [] 158 | 159 | # If not None, a 'Last updated on:' timestamp is inserted at every page 160 | # bottom, using the given strftime format. 161 | # The empty string is equivalent to '%b %d, %Y'. 162 | #html_last_updated_fmt = None 163 | 164 | # If true, SmartyPants will be used to convert quotes and dashes to 165 | # typographically correct entities. 166 | #html_use_smartypants = True 167 | 168 | # Custom sidebar templates, maps document names to template names. 169 | #html_sidebars = {} 170 | 171 | # Additional templates that should be rendered to pages, maps page names to 172 | # template names. 173 | #html_additional_pages = {} 174 | 175 | # If false, no module index is generated. 176 | #html_domain_indices = True 177 | 178 | # If false, no index is generated. 179 | #html_use_index = True 180 | 181 | # If true, the index is split into individual pages for each letter. 182 | #html_split_index = False 183 | 184 | # If true, links to the reST sources are added to the pages. 185 | #html_show_sourcelink = True 186 | 187 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 188 | #html_show_sphinx = True 189 | 190 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 191 | #html_show_copyright = True 192 | 193 | # If true, an OpenSearch description file will be output, and all pages will 194 | # contain a tag referring to it. The value of this option must be the 195 | # base URL from which the finished HTML is served. 196 | #html_use_opensearch = '' 197 | 198 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 199 | #html_file_suffix = None 200 | 201 | # Language to be used for generating the HTML full-text search index. 202 | # Sphinx supports the following languages: 203 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 204 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 205 | #html_search_language = 'en' 206 | 207 | # A dictionary with options for the search language support, empty by default. 208 | # 'ja' uses this config value. 209 | # 'zh' user can custom change `jieba` dictionary path. 210 | #html_search_options = {'type': 'default'} 211 | 212 | # The name of a javascript file (relative to the configuration directory) that 213 | # implements a search results scorer. If empty, the default will be used. 214 | #html_search_scorer = 'scorer.js' 215 | 216 | # Output file base name for HTML help builder. 217 | htmlhelp_basename = 'vectormathdoc' 218 | 219 | # -- Options for LaTeX output --------------------------------------------- 220 | 221 | latex_elements = { 222 | # The paper size ('letterpaper' or 'a4paper'). 223 | #'papersize': 'letterpaper', 224 | 225 | # The font size ('10pt', '11pt' or '12pt'). 226 | #'pointsize': '10pt', 227 | 228 | # Additional stuff for the LaTeX preamble. 229 | #'preamble': '', 230 | 231 | # Latex figure (float) alignment 232 | #'figure_align': 'htbp', 233 | } 234 | 235 | # Grouping the document tree into LaTeX files. List of tuples 236 | # (source start file, target name, title, 237 | # author, documentclass [howto, manual, or own class]). 238 | latex_documents = [ 239 | (master_doc, 'vectormath.tex', u'vectormath Documentation', 240 | u'Seequent', 'manual'), 241 | ] 242 | 243 | # The name of an image file (relative to this directory) to place at the top of 244 | # the title page. 245 | #latex_logo = None 246 | 247 | # For "manual" documents, if this is true, then toplevel headings are parts, 248 | # not chapters. 249 | #latex_use_parts = False 250 | 251 | # If true, show page references after internal links. 252 | #latex_show_pagerefs = False 253 | 254 | # If true, show URL addresses after external links. 255 | #latex_show_urls = False 256 | 257 | # Documents to append as an appendix to all manuals. 258 | #latex_appendices = [] 259 | 260 | # If false, no module index is generated. 261 | #latex_domain_indices = True 262 | 263 | 264 | # -- Options for manual page output --------------------------------------- 265 | 266 | # One entry per manual page. List of tuples 267 | # (source start file, name, description, authors, manual section). 268 | man_pages = [ 269 | (master_doc, 'vectormath', u'vectormath Documentation', 270 | [author], 1) 271 | ] 272 | 273 | # If true, show URL addresses after external links. 274 | #man_show_urls = False 275 | 276 | 277 | # -- Options for Texinfo output ------------------------------------------- 278 | 279 | # Grouping the document tree into Texinfo files. List of tuples 280 | # (source start file, target name, title, author, 281 | # dir menu entry, description, category) 282 | texinfo_documents = [ 283 | (master_doc, 'vectormath', u'vectormath Documentation', 284 | author, 'vectormath', 'One line description of project.', 285 | 'Miscellaneous'), 286 | ] 287 | 288 | # Documents to append as an appendix to all manuals. 289 | #texinfo_appendices = [] 290 | 291 | # If false, no module index is generated. 292 | #texinfo_domain_indices = True 293 | 294 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 295 | #texinfo_show_urls = 'footnote' 296 | 297 | # If true, do not generate a @detailmenu in the "Top" node's menu. 298 | #texinfo_no_detailmenu = False 299 | 300 | # Example configuration for intersphinx: refer to the Python standard library. 301 | intersphinx_mapping = {'https://docs.python.org/3/': None, 302 | # 'https://docs.scipy.org/doc/numpy/': None, 303 | # 'https://docs.scipy.org/doc/scipy/reference/': None, 304 | } 305 | linkcheck_retries = 10 306 | 307 | import sphinx.environment 308 | from docutils.utils import get_source_line 309 | 310 | 311 | def _supress_nonlocal_image_warn(self, msg, node, **kwargs): 312 | if not msg.startswith('nonlocal image URI found:'): 313 | self._warnfunc(msg, '{:s}:{:s}'.format(get_source_line(node), **kwargs)) 314 | 315 | sphinx.environment.BuildEnvironment.warn_node = _supress_nonlocal_image_warn 316 | -------------------------------------------------------------------------------- /docs/content/basic.rst: -------------------------------------------------------------------------------- 1 | .. _basic: 2 | 3 | Basic Vector Math 4 | ================= 5 | 6 | BaseVector 7 | ---------- 8 | 9 | .. autoclass:: vectormath.vector.BaseVector 10 | :members: 11 | :undoc-members: 12 | 13 | BaseVectorArray 14 | --------------- 15 | 16 | .. autoclass:: vectormath.vector.BaseVectorArray 17 | :members: 18 | :undoc-members: 19 | 20 | Vector3 21 | ------- 22 | 23 | .. autoclass:: vectormath.vector.Vector3 24 | :members: 25 | :undoc-members: 26 | 27 | Vector2 28 | ------- 29 | 30 | .. autoclass:: vectormath.vector.Vector2 31 | :members: 32 | :undoc-members: 33 | 34 | Vector3Array 35 | ------------ 36 | 37 | .. autoclass:: vectormath.vector.Vector3Array 38 | :members: 39 | :undoc-members: 40 | 41 | Vector2Array 42 | ------------ 43 | 44 | .. autoclass:: vectormath.vector.Vector2Array 45 | :members: 46 | :undoc-members: 47 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | .. include:: ../README.rst 4 | 5 | Contents: 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | content/basic.rst 11 | 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | 20 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\vectormath.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\vectormath.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | nose-cov 2 | python-coveralls 3 | sphinx 4 | pylint 5 | -e . 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Vector math utilities for Python 4 | """ 5 | 6 | from distutils.core import setup 7 | from setuptools import find_packages 8 | 9 | CLASSIFIERS = [ 10 | 'Development Status :: 4 - Beta', 11 | 'Programming Language :: Python', 12 | 'Topic :: Scientific/Engineering', 13 | 'Topic :: Scientific/Engineering :: Mathematics', 14 | 'Topic :: Scientific/Engineering :: Physics', 15 | 'Operating System :: Microsoft :: Windows', 16 | 'Operating System :: POSIX', 17 | 'Operating System :: Unix', 18 | 'Operating System :: MacOS', 19 | 'Natural Language :: English', 20 | ] 21 | 22 | with open('README.rst') as f: 23 | LONG_DESCRIPTION = ''.join(f.readlines()) 24 | 25 | setup( 26 | name='vectormath', 27 | version='0.2.2', 28 | packages=find_packages(), 29 | install_requires=[ 30 | 'numpy>=1.7', 31 | ], 32 | author='Seequent', 33 | author_email='it@seequent.com', 34 | description='vectormath: vector math utilities for Python', 35 | long_description=LONG_DESCRIPTION, 36 | keywords='linear algebra, vector, plane, math', 37 | url='https://github.com/seequent/vectormath', 38 | download_url='https://github.com/seequent/vectormath', 39 | classifiers=CLASSIFIERS, 40 | platforms=['Windows', 'Linux', 'Solaris', 'Mac OS-X', 'Unix'], 41 | license='MIT License', 42 | include_package_data=True, 43 | use_2to3=False, 44 | ) 45 | -------------------------------------------------------------------------------- /tests/test_docs.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import subprocess 7 | import unittest 8 | import os 9 | 10 | 11 | def docs_dir(): 12 | dirname, filename = os.path.split(os.path.abspath(__file__)) 13 | return os.path.sep.join(dirname.split(os.path.sep)[:-1] + ['docs']) 14 | 15 | 16 | class TestDocs(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.build_dir = os.path.sep.join( 20 | docs_dir().split(os.path.sep) + ['_build'] 21 | ) 22 | if not os.path.isdir(self.build_dir): 23 | subprocess.call(["mkdir", "{0}".format(self.build_dir)]) 24 | 25 | self.doctrees_dir = os.path.sep.join( 26 | self.build_dir.split(os.path.sep) + ['doctrees'] 27 | ) 28 | if not os.path.isdir(self.doctrees_dir): 29 | subprocess.call(["mkdir", "{0}".format(self.doctrees_dir)]) 30 | 31 | self.html_dir = os.path.sep.join( 32 | self.build_dir.split(os.path.sep) + ['html'] 33 | ) 34 | if not os.path.isdir(self.html_dir): 35 | subprocess.call(["mkdir", "{0}".format(self.html_dir)]) 36 | 37 | def test_html(self): 38 | check = subprocess.call([ 39 | "sphinx-build", "-nW", "-b", "html", "-d", 40 | "{}".format(self.doctrees_dir), 41 | "{}".format(docs_dir()), 42 | "{}".format(self.html_dir) 43 | ]) 44 | assert check == 0 45 | 46 | def test_linkcheck(self): 47 | check = subprocess.call([ 48 | "sphinx-build", "-nW", "-b", "linkcheck", "-d", 49 | "{}".format(self.doctrees_dir), 50 | "{}".format(docs_dir()), 51 | "{}".format(self.build_dir) 52 | ]) 53 | assert check == 0 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /tests/test_vector2.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest 7 | import numpy as np 8 | 9 | from vectormath import Vector2, Vector2Array, Vector3, Vector3Array 10 | 11 | 12 | class TestVMathVector2(unittest.TestCase): 13 | 14 | def test_init_exceptions(self): 15 | self.assertRaises(TypeError, Vector2Array, np.r_[1], 1.0) 16 | self.assertRaises(ValueError, Vector2Array, np.r_[1, 2], np.r_[1]) 17 | self.assertRaises(ValueError, Vector2Array, np.array([0, 0, 0])) 18 | self.assertRaises(ValueError, Vector2Array, 'Make', ' me a ') 19 | self.assertRaises(ValueError, Vector2Array, ([0, 0, 0], [0, 0, 0])) 20 | 21 | def test_init(self): 22 | v1 = Vector2Array() 23 | v2 = Vector2Array(0, 0) 24 | self.assertTrue(np.array_equal(v1, v2)) 25 | v3 = Vector2Array(v1) 26 | self.assertTrue(np.array_equal(v1, v3)) 27 | self.assertTrue(v1 is not v3) 28 | v4 = Vector2Array(np.r_[0, 0]) 29 | self.assertTrue(np.array_equal(v1, v4)) 30 | v5 = Vector2Array(np.c_[np.r_[1, 0], np.r_[0, 1]]) 31 | self.assertTrue(np.array_equal(v5.length, np.r_[1, 1])) 32 | v6 = Vector2Array(np.r_[1, 0], np.r_[0, 1]) 33 | self.assertTrue(np.array_equal(v6.length, np.r_[1, 1])) 34 | v7 = Vector2Array([0, 0]) 35 | self.assertTrue(np.array_equal(v1, v7)) 36 | v8 = Vector2Array(x=0, y=0) 37 | self.assertTrue(np.array_equal(v1, v8)) 38 | v9 = Vector2Array( 39 | [0, 0, 0, 0, 0], 40 | [0, 0, 0, 0, 0] 41 | ) 42 | v10 = Vector2Array([ 43 | [0, 0], 44 | [0, 0], 45 | [0, 0], 46 | [0, 0], 47 | [0, 0] 48 | ]) 49 | self.assertTrue(np.array_equal(v9, v10)) 50 | v11 = Vector2Array([[[[[0]], [[0]], ]]]) 51 | self.assertTrue(np.array_equal(v1, v11)) 52 | v12 = Vector2Array([0]*5, [0]*5) 53 | self.assertTrue(np.array_equal(v10, v12)) 54 | v13 = Vector2Array((0, 0)) 55 | self.assertTrue(np.array_equal(v1, v13)) 56 | v14 = Vector2Array(([0, 0], [0, 0])) 57 | self.assertTrue(np.array_equal(v14, Vector2Array([0]*2, [0]*2))) 58 | 59 | def test_indexing(self): 60 | v2 = Vector2Array(1, 2) 61 | self.assertTrue(v2[0, 0] == 1) 62 | self.assertTrue(v2[0, 1] == 2) 63 | self.assertTrue(len(v2[0]) == 2) 64 | 65 | def f(): v2[3] 66 | self.assertRaises(IndexError, f) 67 | 68 | def f(): v2[0, 3] 69 | self.assertRaises(IndexError, f) 70 | l = [] 71 | for x in v2[0]: 72 | l.append(x) 73 | self.assertTrue(np.array_equal(np.array(l), np.r_[1, 2])) 74 | self.assertTrue(np.array_equal(v2, Vector2Array(l))) 75 | l = [] 76 | v3 = Vector2Array([[1, 2], 77 | [2, 3]]) 78 | for v in v3: 79 | l.append(v) 80 | self.assertTrue(np.array_equal( 81 | np.array(l), 82 | np.array([[1, 2], [2, 3]])) 83 | ) 84 | self.assertTrue(np.array_equal(Vector2Array(l), v3)) 85 | v4 = Vector2Array() 86 | v4[0, 0] = 1 87 | v4[0, 1] = 2 88 | self.assertTrue(np.array_equal(v2, v4)) 89 | 90 | def test_copy(self): 91 | vOrig = Vector2Array() 92 | vCopy = vOrig.copy() 93 | self.assertTrue(np.array_equal(vOrig, vCopy)) 94 | self.assertTrue(vOrig is not vCopy) 95 | 96 | def test_size(self): 97 | v1 = Vector2Array() 98 | self.assertTrue(v1.nV == 1) 99 | v2 = Vector2Array(np.c_[np.r_[1, 0, 0], np.r_[0, 1, 0]]) 100 | self.assertTrue(v2.nV == 3) 101 | v3 = Vector2Array( 102 | [0, 0, 0, 0, 0], 103 | [0, 0, 0, 0, 0] 104 | ) 105 | self.assertTrue(v3.nV == 5) 106 | v4 = Vector2Array(0, 0) 107 | self.assertTrue(v4.nV == 1) 108 | 109 | def test_setget(self): 110 | v1 = Vector2Array(1, 1) 111 | self.assertTrue(v1.x == 1) 112 | v1.x = 2 113 | self.assertTrue(v1.x == 2) 114 | self.assertTrue(v1.y == 1) 115 | v1.y = 2 116 | self.assertTrue(v1.y == 2) 117 | 118 | v2 = Vector2Array([[0, 1], 119 | [1, 2]]) 120 | self.assertTrue(np.array_equal(v2.x, [0, 1])) 121 | v2.x = [0, -1] 122 | self.assertTrue(np.array_equal(v2.x, [0, -1])) 123 | self.assertTrue(np.array_equal(v2.y, [1, 2])) 124 | v2.y = [-1, -2] 125 | self.assertTrue(np.array_equal(v2.y, [-1, -2])) 126 | 127 | def test_length(self): 128 | v1 = Vector2Array(1, 1) 129 | self.assertTrue(v1.length == np.sqrt(2)) 130 | v2 = Vector2Array(np.r_[1, 2], np.r_[1, 2]) 131 | self.assertTrue(np.array_equal(v2.length, np.sqrt(np.r_[2, 8]))) 132 | v3 = Vector2Array(1, 0) 133 | v3.length = 5 134 | assert v3.x == 5 135 | self.assertTrue(v3.length == 5) 136 | v4 = Vector2Array(np.r_[1, 1], np.r_[0, 0]) 137 | 138 | def f(): v4.length = 5 139 | self.assertRaises(ValueError, f) 140 | v5 = Vector2Array(np.r_[1, 0], np.r_[0, 1]) 141 | self.assertTrue(np.array_equal(v5.length, [1, 1])) 142 | v5.length = [-1, 3] 143 | self.assertTrue(np.array_equal(v5, [[-1., -0.], [0., 3.]])) 144 | self.assertTrue(np.array_equal(v5.length, [1, 3])) 145 | v6 = Vector2Array() 146 | self.assertTrue(v6.length == 0) 147 | 148 | def f(): v6.length = 5 149 | self.assertRaises(ZeroDivisionError, f) 150 | v6.length = 0 151 | self.assertTrue(v6.length == 0) 152 | v7 = Vector2Array( 153 | [0, 0, 1, 0, 0], 154 | [1, 1, 0, 0, 0] 155 | ) 156 | length = [5, 5, 5, 5, 5] 157 | 158 | def f(): v7.length = length 159 | self.assertRaises(ZeroDivisionError, f) 160 | length = [5, 5, 5, 0, 0] 161 | v7.length = length 162 | self.assertTrue(np.array_equal(length, v7.length)) 163 | 164 | def test_ops(self): 165 | v1 = Vector2Array(1, 1) 166 | v2 = Vector2Array(2, 2) 167 | self.assertTrue(np.array_equal(v2-v1, v1)) 168 | self.assertTrue(np.array_equal(v1-v2, -v1)) 169 | self.assertTrue(np.array_equal(v1+v1, v2)) 170 | self.assertTrue(np.array_equal(v1*v2, v2)) 171 | self.assertTrue(np.array_equal(v2/v1, v2)) 172 | self.assertTrue(np.array_equal(2*v1, v2)) 173 | self.assertTrue(np.array_equal(v2/2, v1)) 174 | self.assertTrue(np.array_equal(v1+1, v2)) 175 | self.assertTrue(np.array_equal(v2-1, v1)) 176 | v1 = Vector2Array(np.r_[1, 1.], np.r_[1, 1.]) 177 | v2 = Vector2Array(np.r_[2, 2.], np.r_[2, 2.]) 178 | self.assertTrue(np.array_equal(v2-v1, v1)) 179 | self.assertTrue(np.array_equal(v1-v2, -v1)) 180 | self.assertTrue(np.array_equal(v1+v1, v2)) 181 | self.assertTrue(np.array_equal(v1*v2, v2)) 182 | self.assertTrue(np.array_equal(v2/v1, v2)) 183 | self.assertTrue(np.array_equal(2*v1, v2)) 184 | self.assertTrue(np.array_equal(v2/2, v1)) 185 | self.assertTrue(np.array_equal(v1+1, v2)) 186 | self.assertTrue(np.array_equal(v2-1, v1)) 187 | 188 | def test_dot(self): 189 | v1 = Vector2Array(1, 1) 190 | v2 = Vector2Array(2, 2) 191 | self.assertTrue(v1.dot(v2) == 4) 192 | v1l = Vector2Array(np.r_[1, 1.], np.r_[1, 1.]) 193 | v2l = Vector2Array(np.r_[2, 2.], np.r_[2, 2.]) 194 | self.assertTrue(np.array_equal(v1l.dot(v2l), np.r_[4, 4])) 195 | self.assertTrue(np.array_equal(v1.dot(v2l), np.r_[4, 4])) 196 | self.assertTrue(np.array_equal(v1l.dot(v2), np.r_[4, 4])) 197 | v3 = Vector2Array([3]*4, [3]*4) 198 | 199 | def f(): v3.dot(v2l) 200 | self.assertRaises(ValueError, f) 201 | 202 | def f(): v3.dot(5) 203 | self.assertRaises(TypeError, f) 204 | 205 | def test_as_percent(self): 206 | v1 = Vector2Array(10, 0) 207 | v2 = Vector2Array(20, 0) 208 | self.assertTrue(np.array_equal(v1.as_percent(2), v2)) 209 | self.assertTrue(np.array_equal(v1, Vector2Array(10, 0))) # not copied 210 | v3 = Vector2Array( 211 | [0, 0, 2, 0, 0], 212 | [0, 2, 0, 0, 0] 213 | ) 214 | v4 = v3 * .5 215 | self.assertTrue(np.array_equal(v3.as_percent(.5), v4)) 216 | v5 = Vector2Array() 217 | self.assertTrue(np.array_equal(v5.as_percent(100), v5)) 218 | v6 = Vector2Array(5, 5) 219 | self.assertTrue(np.array_equal(v6.as_percent(0), v5)) 220 | 221 | def f(): v6.as_percent('One Hundred Percent') 222 | self.assertRaises(TypeError, f) 223 | 224 | def test_normalize(self): 225 | v1 = Vector2Array(5, 0) 226 | self.assertTrue(v1.length == 5) 227 | self.assertTrue(v1.normalize() is v1) 228 | self.assertTrue(v1.length == 1) 229 | v2 = Vector2Array() 230 | 231 | def f(): v2.normalize() 232 | self.assertRaises(ZeroDivisionError, f) 233 | v3 = Vector2Array( 234 | [0, 2], 235 | [2, 0] 236 | ) 237 | self.assertTrue(np.array_equal(v3.length, [2, 2])) 238 | self.assertTrue(v3.normalize() is v3) 239 | self.assertTrue(np.array_equal(v3.length, [1, 1])) 240 | 241 | def test_as_length(self): 242 | v1 = Vector2Array(1, 1) 243 | v2 = v1.as_length(1) 244 | self.assertTrue(v1 is not v2) 245 | self.assertTrue(v1.length == np.sqrt(2)) 246 | self.assertAlmostEqual(v2.length[0], 1) 247 | 248 | v3 = Vector2Array(np.r_[1, 2], np.r_[1, 2]) 249 | v4 = v3.as_length([1, 2]) 250 | self.assertTrue(np.allclose(v4.length, [1, 2])) 251 | 252 | def f(): v = v3.as_length(5) 253 | self.assertRaises(ValueError, f) 254 | v5 = Vector2Array(np.r_[1, 0], np.r_[0, 1]) 255 | self.assertTrue(np.allclose(v5.length, [1, 1])) 256 | v6 = v5.as_length([-1, 3]) 257 | self.assertTrue(np.allclose(v6, [[-1., -0.], [0., 3.]])) 258 | self.assertTrue(np.allclose(v6.length, [1, 3])) 259 | v7 = Vector2Array() 260 | 261 | def f(): v = v7.as_length(5) 262 | self.assertRaises(ZeroDivisionError, f) 263 | v8 = v7.as_length(0) 264 | self.assertTrue(v8.length == 0) 265 | v9 = Vector2Array( 266 | [0, 0, 1, 0, 0], 267 | [1, 1, 0, 0, 0] 268 | ) 269 | length = [5, 5, 5, 5, 5] 270 | 271 | def f(): v = v9.as_length(length) 272 | self.assertRaises(ZeroDivisionError, f) 273 | length = [5, 5, 5, 0, 0] 274 | v10 = v9.as_length(length) 275 | self.assertTrue(np.array_equal(length, v10.length)) 276 | 277 | def test_as_unit(self): 278 | v1 = Vector2Array(1, 0) 279 | v2 = v1.as_unit() 280 | self.assertTrue(v1 is not v2) 281 | self.assertTrue(np.array_equal(v1, v2)) 282 | self.assertTrue(v2.length == 1) 283 | v3 = Vector2Array(np.r_[1, 2], np.r_[1, 2]) 284 | v4 = v3.as_unit() 285 | self.assertTrue(np.allclose(v4.length, [1, 1])) 286 | v5 = Vector2Array(1, 1) 287 | v6 = v5.as_unit() 288 | self.assertAlmostEqual(v6.length[0], 1) 289 | self.assertTrue(v6.x == v6.y) 290 | v7 = Vector2Array() 291 | 292 | def f(): v = v7.as_unit() 293 | self.assertRaises(ZeroDivisionError, f) 294 | v9 = Vector2Array( 295 | [0, 0, 1, 0, 0], 296 | [0, 1, 0, 0, 0] 297 | ) 298 | 299 | def f(): v = v9.as_unit() 300 | self.assertRaises(ZeroDivisionError, f) 301 | 302 | def test_view_types(self): 303 | v1 = Vector2Array(np.random.rand(100, 2)) 304 | self.assertTrue(isinstance(v1, Vector2Array)) 305 | self.assertTrue(isinstance(v1[1:2], Vector2Array)) 306 | self.assertTrue(isinstance(v1[1:50:2], Vector2Array)) 307 | self.assertTrue(isinstance(v1[4], Vector2)) 308 | self.assertTrue(isinstance(v1[4, :], np.ndarray)) 309 | self.assertTrue(isinstance(v1.x, np.ndarray)) 310 | self.assertTrue(isinstance(v1[1:30, :], np.ndarray)) 311 | 312 | a1 = np.array([1., 2., 3]) 313 | with self.assertRaises(ValueError): 314 | a1.view(Vector2) 315 | with self.assertRaises(ValueError): 316 | a1.view(Vector2Array) 317 | a1 = np.array([1., 2.]) 318 | self.assertTrue(isinstance(a1.view(Vector2), Vector2)) 319 | with self.assertRaises(ValueError): 320 | a1.view(Vector2Array) 321 | a1 = np.array([[1., 2.]]) 322 | with self.assertRaises(ValueError): 323 | a1.view(Vector2) 324 | self.assertTrue(isinstance(a1.view(Vector2Array), Vector2Array)) 325 | 326 | with self.assertRaises(ValueError): 327 | v1.view(Vector3Array) 328 | self.assertTrue(isinstance(v1.view(Vector2Array), Vector2Array)) 329 | with self.assertRaises(ValueError): 330 | v1.view(Vector3) 331 | with self.assertRaises(ValueError): 332 | v1.view(Vector2) 333 | v1 = Vector2([1., 2.]) 334 | with self.assertRaises(ValueError): 335 | v1.view(Vector3Array) 336 | with self.assertRaises(ValueError): 337 | v1.view(Vector2Array) 338 | with self.assertRaises(ValueError): 339 | v1.view(Vector3) 340 | self.assertTrue(isinstance(v1.view(Vector2), Vector2)) 341 | 342 | v1 = np.kron(Vector2([1., 0.]), np.atleast_2d(np.ones(10)).T) 343 | self.assertFalse(isinstance(v1, Vector2)) 344 | 345 | def test_cross(self): 346 | vector2 = Vector2(5, 0) 347 | vector2_2 = Vector2(1, 0) 348 | crossResult = vector2.cross(vector2_2) 349 | self.assertEqual(crossResult[0], 0) 350 | self.assertEqual(crossResult[1], 0) 351 | self.assertEqual(crossResult[2], 0) 352 | with self.assertRaises(TypeError): 353 | dotResult2 = vector2.cross("Banana") 354 | 355 | def test_angle(self): 356 | 357 | # test a unit vector along each coordinate 358 | v1 = Vector2(1, 0) # x-coordinate, use this as datum 359 | v = [Vector2(1, 0), Vector2(0, 1), Vector2(-1, 0), Vector2(0, -1)] 360 | angles_deg = [0, 90, 180, 90] 361 | angles_rad = [0, np.pi / 2, np.pi, np.pi / 2] 362 | for k in range(4): 363 | a_deg = v1.angle(v[k], unit='deg') 364 | a_rad0 = v1.angle(v[k], unit='rad') 365 | a_rad1 = v1.angle(v[k]) 366 | self.assertEqual(a_deg, angles_deg[k]) 367 | self.assertEqual(a_rad0, angles_rad[k]) 368 | self.assertEqual(a_rad1, angles_rad[k]) 369 | 370 | # verify the associative property 371 | self.assertEqual(v1.angle(v[k]), v[k].angle(v1)) 372 | 373 | with self.assertRaises(TypeError): 374 | angleResult = v1.angle('anything but Vector2') 375 | with self.assertRaises(ValueError): 376 | angleResult = v1.angle(v[0], unit='invalid entry') 377 | with self.assertRaises(ZeroDivisionError): 378 | angleResult = v1.angle(Vector2(0, 0)) 379 | 380 | def test_polar(self): 381 | # polar <-> cartesian conversions 382 | cases = [ 383 | # ((rho, theta), (x, y)) 384 | ((1, 0), (1, 0)), 385 | ((1, np.pi), (-1, 0)), 386 | ((2, -np.pi / 2), (0, -2)), 387 | ((1, np.pi * 3 / 4), (-1 / np.sqrt(2), 1 / np.sqrt(2))), 388 | ((3, np.pi / 4), (3 / np.sqrt(2), 3 / np.sqrt(2))), 389 | ] 390 | for polar, cartesian in cases: 391 | rho, theta = polar 392 | x, y = cartesian 393 | v = Vector2(rho, theta, polar=True) 394 | self.assertAlmostEqual(v.x, x) 395 | self.assertAlmostEqual(v.y, y) 396 | v = Vector2(x, y) 397 | self.assertAlmostEqual(v.rho, rho) 398 | self.assertAlmostEqual(v.theta, theta) 399 | 400 | # degrees -> radians 401 | cases = [ 402 | # (degrees, radians) 403 | (0, 0), 404 | (90, np.pi/2), 405 | (-90, -np.pi/2), 406 | (45, np.pi/4), 407 | (180, np.pi), 408 | ] 409 | for deg, rad in cases: 410 | v = Vector2(1, deg, polar=True, unit='deg') 411 | self.assertAlmostEqual(v.theta, rad) 412 | self.assertAlmostEqual(v.theta_deg, deg) 413 | 414 | # faulty input 415 | with self.assertRaises(ValueError): 416 | Vector2(1, np.pi, polar=True, unit='invalid_unit') 417 | with self.assertRaises(ValueError): 418 | v = Vector2(1, np.pi, polar=True) 419 | # copying doesn't support polar=True 420 | Vector2(v, polar=True) 421 | 422 | def test_spherical(self): 423 | # cartesian -> sperical conversions 424 | cases = [ 425 | # ((x, y, z), (rho, theta, phi)) 426 | ((1, 0, 0), (1, 0, np.pi/2)), 427 | ((1, 0, 1), (np.sqrt(2), 0, np.pi/4)), 428 | ((1, 0, -1), (np.sqrt(2), 0, np.pi/4*3)), 429 | ] 430 | for cartesian, sperical in cases: 431 | rho, theta, phi = sperical 432 | x, y, z = cartesian 433 | v = Vector3(x, y, z) 434 | self.assertAlmostEqual(v.rho, rho) 435 | self.assertAlmostEqual(v.theta, theta) 436 | self.assertAlmostEqual(v.phi, phi) 437 | 438 | 439 | if __name__ == '__main__': 440 | unittest.main() 441 | -------------------------------------------------------------------------------- /tests/test_vector3.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest 7 | import numpy as np 8 | 9 | from vectormath import Vector2, Vector2Array, Vector3, Vector3Array 10 | 11 | 12 | class TestVMathVector3(unittest.TestCase): 13 | 14 | def test_init_exceptions(self): 15 | self.assertRaises(TypeError, Vector3Array, np.r_[1], np.r_[1], 3) 16 | self.assertRaises(ValueError, 17 | Vector3Array, np.r_[1, 2], np.r_[1], np.r_[1]) 18 | self.assertRaises(ValueError, Vector3Array, np.array([0, 0])) 19 | self.assertRaises(ValueError, 20 | Vector3Array, 'Make', ' me a ', 'vector!') 21 | self.assertRaises(ValueError, Vector3Array, ([0, 0], [0, 0], [0, 0])) 22 | 23 | def test_init(self): 24 | v1 = Vector3Array() 25 | v2 = Vector3Array(0, 0, 0) 26 | self.assertTrue(np.array_equal(v1, v2)) 27 | v3 = Vector3Array(v1) 28 | self.assertTrue(np.array_equal(v1, v3)) 29 | self.assertTrue(v1 is not v3) 30 | v4 = Vector3Array(np.r_[0, 0, 0]) 31 | self.assertTrue(np.array_equal(v1, v4)) 32 | v5 = Vector3Array(np.c_[np.r_[1, 0, 0], 33 | np.r_[0, 1, 0], 34 | np.r_[0, 0, 1]]) 35 | self.assertTrue(np.array_equal(v5.length, np.r_[1, 1, 1])) 36 | v6 = Vector3Array(np.r_[1, 0, 0], np.r_[0, 1, 0], np.r_[0, 0, 1]) 37 | self.assertTrue(np.array_equal(v6.length, np.r_[1, 1, 1])) 38 | v7 = Vector3Array([0, 0, 0]) 39 | self.assertTrue(np.array_equal(v1, v7)) 40 | v8 = Vector3Array(x=0, y=0, z=0) 41 | self.assertTrue(np.array_equal(v1, v8)) 42 | v9 = Vector3Array( 43 | [0, 0, 0, 0, 0], 44 | [0, 0, 0, 0, 0], 45 | [0, 0, 0, 0, 0] 46 | ) 47 | v10 = Vector3Array([ 48 | [0, 0, 0], 49 | [0, 0, 0], 50 | [0, 0, 0], 51 | [0, 0, 0], 52 | [0, 0, 0] 53 | ]) 54 | self.assertTrue(np.array_equal(v9, v10)) 55 | v11 = Vector3Array([[[[[0]], [[0]], [[0]]]]]) 56 | self.assertTrue(np.array_equal(v1, v11)) 57 | v12 = Vector3Array([0]*5, [0]*5, [0]*5) 58 | self.assertTrue(np.array_equal(v10, v12)) 59 | v13 = Vector3Array((0, 0, 0)) 60 | self.assertTrue(np.array_equal(v1, v13)) 61 | v14 = Vector3Array(([0, 0, 0], [0, 0, 0])) 62 | self.assertTrue(np.array_equal(v14, Vector3Array([0]*2, [0]*2, [0]*2))) 63 | 64 | def test_indexing(self): 65 | v2 = Vector3Array(1, 2, 3) 66 | self.assertTrue(v2[0, 0] == 1) 67 | self.assertTrue(v2[0, 1] == 2) 68 | self.assertTrue(v2[0, 2] == 3) 69 | self.assertTrue(len(v2[0]) == 3) 70 | 71 | self.assertRaises(IndexError, lambda: v2[3]) 72 | self.assertRaises(IndexError, lambda: v2[0, 3]) 73 | l = [] 74 | for x in v2[0]: 75 | l.append(x) 76 | self.assertTrue(np.array_equal(np.array(l), np.r_[1, 2, 3])) 77 | self.assertTrue(np.array_equal(v2, Vector3Array(l))) 78 | l = [] 79 | v3 = Vector3Array([[1, 2, 3], 80 | [2, 3, 4]]) 81 | for v in v3: 82 | l.append(v) 83 | self.assertTrue(np.array_equal( 84 | np.array(l), 85 | np.array([[1, 2, 3], [2, 3, 4]])) 86 | ) 87 | self.assertTrue(np.array_equal(Vector3Array(l), v3)) 88 | v4 = Vector3Array() 89 | v4[0, 0] = 1 90 | v4[0, 1] = 2 91 | v4[0, 2] = 3 92 | self.assertTrue(np.array_equal(v2, v4)) 93 | 94 | def test_copy(self): 95 | vOrig = Vector3Array() 96 | vCopy = vOrig.copy() 97 | self.assertTrue(np.array_equal(vOrig, vCopy)) 98 | self.assertTrue(vOrig is not vCopy) 99 | 100 | def test_size(self): 101 | v1 = Vector3Array() 102 | self.assertTrue(v1.nV == 1) 103 | v2 = Vector3Array(np.c_[np.r_[1, 0, 0], 104 | np.r_[0, 1, 0], 105 | np.r_[0, 0, 1]]) 106 | self.assertTrue(v2.nV == 3) 107 | v3 = Vector3Array( 108 | [0, 0, 0, 0, 0], 109 | [0, 0, 0, 0, 0], 110 | [0, 0, 0, 0, 0] 111 | ) 112 | self.assertTrue(v3.nV == 5) 113 | v4 = Vector3Array(0, 0, 0) 114 | self.assertTrue(v4.nV == 1) 115 | 116 | def test_setget(self): 117 | v1 = Vector3Array(1, 1, 1) 118 | self.assertTrue(v1.x == 1) 119 | v1.x = 2 120 | self.assertTrue(v1.x == 2) 121 | self.assertTrue(v1.y == 1) 122 | v1.y = 2 123 | self.assertTrue(v1.y == 2) 124 | self.assertTrue(v1.z == 1) 125 | v1.z = 2 126 | self.assertTrue(v1.z == 2) 127 | v2 = Vector3Array([[0, 1, 2], 128 | [1, 2, 3]]) 129 | self.assertTrue(np.array_equal(v2.x, [0, 1])) 130 | v2.x = [0, -1] 131 | self.assertTrue(np.array_equal(v2.x, [0, -1])) 132 | self.assertTrue(np.array_equal(v2.y, [1, 2])) 133 | v2.y = [-1, -2] 134 | self.assertTrue(np.array_equal(v2.y, [-1, -2])) 135 | self.assertTrue(np.array_equal(v2.z, [2, 3])) 136 | v2.z = [0, 0] 137 | self.assertTrue(np.array_equal(v2.z, [0, 0])) 138 | 139 | def test_length(self): 140 | v1 = Vector3Array(1, 1, 1) 141 | self.assertTrue(v1.length == np.sqrt(3)) 142 | v2 = Vector3Array(np.r_[1, 2], np.r_[1, 2], np.r_[1, 2]) 143 | self.assertTrue(np.array_equal(v2.length, np.sqrt(np.r_[3, 12]))) 144 | v3 = Vector3Array(1, 0, 0) 145 | v3.length = 5 146 | self.assertTrue(v3.length == 5) 147 | v4 = Vector3Array(np.r_[1, 1], np.r_[0, 0], np.r_[1, 2]) 148 | 149 | self.assertRaises(ValueError, lambda: setattr(v4, 'length', 5)) 150 | v5 = Vector3Array(np.r_[1, 0], np.r_[0, 0], np.r_[0, 1]) 151 | self.assertTrue(np.array_equal(v5.length, [1, 1])) 152 | v5.length = [-1, 3] 153 | self.assertTrue(np.array_equal(v5, [[-1., -0., -0.], [0., 0., 3.]])) 154 | self.assertTrue(np.array_equal(v5.length, [1, 3])) 155 | v6 = Vector3Array() 156 | self.assertTrue(v6.length == 0) 157 | 158 | self.assertRaises(ZeroDivisionError, lambda: setattr(v6, 'length', 5)) 159 | v6.length = 0 160 | self.assertTrue(v6.length == 0) 161 | v7 = Vector3Array( 162 | [0, 0, 1, 0, 0], 163 | [0, 1, 0, 0, 0], 164 | [1, 0, 0, 0, 0] 165 | ) 166 | length = [5, 5, 5, 5, 5] 167 | 168 | self.assertRaises(ZeroDivisionError, 169 | lambda: setattr(v7, 'length', length)) 170 | length = [5, 5, 5, 0, 0] 171 | v7.length = length 172 | self.assertTrue(np.array_equal(length, v7.length)) 173 | 174 | def test_ops(self): 175 | v1 = Vector3Array(1, 1, 1) 176 | v2 = Vector3Array(2, 2, 2) 177 | self.assertTrue(np.array_equal(v2-v1, v1)) 178 | self.assertTrue(np.array_equal(v1-v2, -v1)) 179 | self.assertTrue(np.array_equal(v1+v1, v2)) 180 | self.assertTrue(np.array_equal(v1*v2, v2)) 181 | self.assertTrue(np.array_equal(v2/v1, v2)) 182 | self.assertTrue(np.array_equal(2*v1, v2)) 183 | self.assertTrue(np.array_equal(v2/2, v1)) 184 | self.assertTrue(np.array_equal(v1+1, v2)) 185 | self.assertTrue(np.array_equal(v2-1, v1)) 186 | v1 = Vector3Array(np.r_[1, 1.], np.r_[1, 1.], np.r_[1, 1.]) 187 | v2 = Vector3Array(np.r_[2, 2.], np.r_[2, 2.], np.r_[2, 2.]) 188 | self.assertTrue(np.array_equal(v2-v1, v1)) 189 | self.assertTrue(np.array_equal(v1-v2, -v1)) 190 | self.assertTrue(np.array_equal(v1+v1, v2)) 191 | self.assertTrue(np.array_equal(v1*v2, v2)) 192 | self.assertTrue(np.array_equal(v2/v1, v2)) 193 | self.assertTrue(np.array_equal(2*v1, v2)) 194 | self.assertTrue(np.array_equal(v2/2, v1)) 195 | self.assertTrue(np.array_equal(v1+1, v2)) 196 | self.assertTrue(np.array_equal(v2-1, v1)) 197 | 198 | def test_dot(self): 199 | v1 = Vector3Array(1, 1, 1) 200 | v2 = Vector3Array(2, 2, 2) 201 | self.assertTrue(v1.dot(v2) == 6) 202 | v1l = Vector3Array(np.r_[1, 1.], np.r_[1, 1.], np.r_[1, 1.]) 203 | v2l = Vector3Array(np.r_[2, 2.], np.r_[2, 2.], np.r_[2, 2.]) 204 | self.assertTrue(np.array_equal(v1l.dot(v2l), np.r_[6, 6])) 205 | self.assertTrue(np.array_equal(v1.dot(v2l), np.r_[6, 6])) 206 | self.assertTrue(np.array_equal(v1l.dot(v2), np.r_[6, 6])) 207 | v3 = Vector3Array([3]*4, [3]*4, [3]*4) 208 | 209 | self.assertRaises(ValueError, lambda: v3.dot(v2l)) 210 | self.assertRaises(TypeError, lambda: v3.dot(5)) 211 | 212 | def test_cross(self): 213 | v1 = Vector3Array(1, 0, 0) 214 | v2 = Vector3Array(0, 1, 0) 215 | vC = Vector3Array(0, 0, 1) 216 | self.assertTrue(np.array_equal(v1.cross(v2), vC)) 217 | v1 = Vector3Array(np.r_[1, 1], np.r_[0, 0], np.r_[0, 0]) 218 | v2 = Vector3Array(np.r_[0, 0], np.r_[1, 1], np.r_[0, 0]) 219 | vC = Vector3Array(np.r_[0, 0], np.r_[0, 0], np.r_[1, 1]) 220 | self.assertTrue(np.array_equal(v1.cross(v2), vC)) 221 | v3 = Vector3Array([3]*4, [3]*4, [3]*4) 222 | 223 | def f(): v3.cross(v2) 224 | self.assertRaises(ValueError, f) 225 | 226 | def f(): v3.cross(5) 227 | self.assertRaises(TypeError, f) 228 | 229 | def test_as_percent(self): 230 | v1 = Vector3Array(10, 0, 0) 231 | v2 = Vector3Array(20, 0, 0) 232 | self.assertTrue(np.array_equal(v1.as_percent(2), v2)) 233 | self.assertTrue(np.array_equal(v1, Vector3Array(10, 0, 0)))# not copied 234 | v3 = Vector3Array( 235 | [0, 0, 2, 0, 0], 236 | [0, 2, 0, 0, 0], 237 | [2, 0, 0, 0, 0] 238 | ) 239 | v4 = v3 * .5 240 | self.assertTrue(np.array_equal(v3.as_percent(.5), v4)) 241 | v5 = Vector3Array() 242 | self.assertTrue(np.array_equal(v5.as_percent(100), v5)) 243 | v6 = Vector3Array(5, 5, 5) 244 | self.assertTrue(np.array_equal(v6.as_percent(0), v5)) 245 | 246 | self.assertRaises(TypeError, 247 | lambda: v6.as_percent('One Hundred Percent')) 248 | 249 | def test_normalize(self): 250 | v1 = Vector3Array(5, 0, 0) 251 | self.assertTrue(v1.length == 5) 252 | self.assertTrue(v1.normalize() is v1) 253 | self.assertTrue(v1.length == 1) 254 | v2 = Vector3Array() 255 | 256 | self.assertRaises(ZeroDivisionError, lambda: v2.normalize()) 257 | v3 = Vector3Array( 258 | [0, 0, 2], 259 | [0, 2, 0], 260 | [2, 0, 0] 261 | ) 262 | self.assertTrue(np.array_equal(v3.length, [2, 2, 2])) 263 | self.assertTrue(v3.normalize() is v3) 264 | self.assertTrue(np.array_equal(v3.length, [1, 1, 1])) 265 | 266 | def test_as_length(self): 267 | v1 = Vector3Array(1, 1, 1) 268 | v2 = v1.as_length(1) 269 | self.assertTrue(v1 is not v2) 270 | self.assertTrue(v1.length == np.sqrt(3)) 271 | self.assertTrue(v2.length == 1) 272 | 273 | v3 = Vector3Array(np.r_[1, 2], np.r_[1, 2], np.r_[1, 2]) 274 | v4 = v3.as_length([1, 2]) 275 | self.assertTrue(np.array_equal(v4.length, [1, 2])) 276 | 277 | self.assertRaises(ValueError, lambda: v3.as_length(5)) 278 | v5 = Vector3Array(np.r_[1, 0], np.r_[0, 0], np.r_[0, 1]) 279 | self.assertTrue(np.array_equal(v5.length, [1, 1])) 280 | v6 = v5.as_length([-1, 3]) 281 | self.assertTrue(np.array_equal(v6, [[-1., -0., -0.], [0., 0., 3.]])) 282 | self.assertTrue(np.array_equal(v6.length, [1, 3])) 283 | v7 = Vector3Array() 284 | 285 | self.assertRaises(ZeroDivisionError, lambda: v7.as_length(5)) 286 | v8 = v7.as_length(0) 287 | self.assertTrue(v8.length == 0) 288 | v9 = Vector3Array( 289 | [0, 0, 1, 0, 0], 290 | [0, 1, 0, 0, 0], 291 | [1, 0, 0, 0, 0] 292 | ) 293 | length = [5, 5, 5, 5, 5] 294 | 295 | self.assertRaises(ZeroDivisionError, lambda: v9.as_length(length)) 296 | length = [5, 5, 5, 0, 0] 297 | v10 = v9.as_length(length) 298 | self.assertTrue(np.array_equal(length, v10.length)) 299 | 300 | def test_as_unit(self): 301 | v1 = Vector3Array(1, 0, 0) 302 | v2 = v1.as_unit() 303 | self.assertTrue(v1 is not v2) 304 | self.assertTrue(np.array_equal(v1, v2)) 305 | self.assertTrue(v2.length == 1) 306 | v3 = Vector3Array(np.r_[1, 2], np.r_[1, 2], np.r_[1, 2]) 307 | v4 = v3.as_unit() 308 | self.assertTrue(np.array_equal(v4.length, [1, 1])) 309 | v5 = Vector3Array(1, 1, 1) 310 | v6 = v5.as_unit() 311 | self.assertTrue(v6.length == 1) 312 | self.assertTrue(v6.x == v6.y) 313 | self.assertTrue(v6.z == v6.y) 314 | v7 = Vector3Array() 315 | 316 | self.assertRaises(ZeroDivisionError, v7.as_unit) 317 | v9 = Vector3Array( 318 | [0, 0, 1, 0, 0], 319 | [0, 1, 0, 0, 0], 320 | [1, 0, 0, 0, 0] 321 | ) 322 | 323 | self.assertRaises(ZeroDivisionError, v9.as_unit) 324 | 325 | def test_view_types(self): 326 | v1 = Vector3Array(np.random.rand(100, 3)) 327 | self.assertTrue(isinstance(v1, Vector3Array)) 328 | self.assertTrue(isinstance(v1[1:2], Vector3Array)) 329 | self.assertTrue(isinstance(v1[1:50:2], Vector3Array)) 330 | self.assertTrue(isinstance(v1[4], Vector3)) 331 | self.assertTrue(isinstance(v1[4, :], np.ndarray)) 332 | self.assertTrue(isinstance(v1.x, np.ndarray)) 333 | self.assertTrue(isinstance(v1[1:30, :], np.ndarray)) 334 | 335 | a1 = np.array([1., 2.]) 336 | with self.assertRaises(ValueError): 337 | a1.view(Vector3) 338 | with self.assertRaises(ValueError): 339 | a1.view(Vector3Array) 340 | a1 = np.array([1., 2., 3.]) 341 | self.assertTrue(isinstance(a1.view(Vector3), Vector3)) 342 | with self.assertRaises(ValueError): 343 | a1.view(Vector3Array) 344 | a1 = np.array([[1., 2., 3.]]) 345 | with self.assertRaises(ValueError): 346 | a1.view(Vector3) 347 | self.assertTrue(isinstance(a1.view(Vector3Array), Vector3Array)) 348 | 349 | self.assertTrue(isinstance(v1.view(Vector3Array), Vector3Array)) 350 | with self.assertRaises(ValueError): 351 | v1.view(Vector2Array) 352 | with self.assertRaises(ValueError): 353 | v1.view(Vector3) 354 | with self.assertRaises(ValueError): 355 | v1.view(Vector2) 356 | v1 = Vector3([1., 2., 3.]) 357 | with self.assertRaises(ValueError): 358 | v1.view(Vector3Array) 359 | with self.assertRaises(ValueError): 360 | v1.view(Vector2Array) 361 | with self.assertRaises(ValueError): 362 | v1.view(Vector2) 363 | self.assertTrue(isinstance(v1.view(Vector3), Vector3)) 364 | 365 | v1 = np.kron(Vector3([1., 0., 0.]), np.atleast_2d(np.ones(10)).T) 366 | self.assertFalse(isinstance(v1, Vector3)) 367 | 368 | def test_angle(self): 369 | 370 | # test a unit vector along each coordinate 371 | v1 = Vector3(1, 0, 0) # x-axis, use this as datum 372 | v = [Vector3(1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, 1), 373 | Vector3(-1, 0, 0), Vector3(0, -1, 0), Vector3(0, 0, -1)] 374 | angles_deg = [0, 90, 90, 180, 90, 90] 375 | angles_rad = [0, np.pi / 2, np.pi / 2, np.pi, np.pi / 2, np.pi / 2] 376 | for k in range(6): 377 | a_deg = v1.angle(v[k], unit='deg') 378 | a_rad0 = v1.angle(v[k], unit='rad') 379 | a_rad1 = v1.angle(v[k]) 380 | self.assertEqual(a_deg, angles_deg[k]) 381 | self.assertEqual(a_rad0, angles_rad[k]) 382 | self.assertEqual(a_rad1, angles_rad[k]) 383 | 384 | # verify the associative property 385 | self.assertEqual(v1.angle(v[k]), v[k].angle(v1)) 386 | 387 | with self.assertRaises(TypeError): 388 | angleResult = v1.angle('anything but Vector3') 389 | with self.assertRaises(ValueError): 390 | angleResult = v1.angle(v[0], unit='invalid entry') 391 | with self.assertRaises(ZeroDivisionError): 392 | angleResult = v1.angle(Vector3(0, 0, 0)) 393 | 394 | # def test_mult_warning(self): 395 | # with warnings.catch_warnings(record=True) as w: 396 | # v1 = Vector3Array() 397 | # v2 = v1 * 3 398 | # self.assertTrue(len(w) == 0) 399 | # M = Matrix3() 400 | # v3 = v2 * M 401 | # self.assertTrue(len(w) == 1) 402 | 403 | 404 | if __name__ == '__main__': 405 | unittest.main() 406 | -------------------------------------------------------------------------------- /vectormath/__init__.py: -------------------------------------------------------------------------------- 1 | """vectormath: Vector math utilities for Python built on NumPy""" 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | from .vector import Vector3, Vector2, Vector3Array, Vector2Array 8 | 9 | __version__ = '0.2.2' 10 | __author__ = 'Seequent' 11 | __license__ = 'MIT' 12 | __copyright__ = 'Copyright 2018 Seequent' 13 | -------------------------------------------------------------------------------- /vectormath/vector.py: -------------------------------------------------------------------------------- 1 | """vector.py contains definitions for Vector and VectorArray classes""" 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import numpy as np 8 | 9 | 10 | class BaseVector(np.ndarray): 11 | """Class to contain basic operations used by all Vector classes""" 12 | 13 | def __new__(cls, *args, **kwargs): 14 | """BaseVector cannot be created""" 15 | raise NotImplementedError('Please specify Vector2 or Vector3') 16 | 17 | @property 18 | def x(self): 19 | """x-component of vector""" 20 | return self[0] 21 | 22 | @x.setter 23 | def x(self, value): 24 | self[0] = value 25 | 26 | @property 27 | def y(self): 28 | """y-component of vector""" 29 | return self[1] 30 | 31 | @y.setter 32 | def y(self, value): 33 | self[1] = value 34 | 35 | @property 36 | def length(self): 37 | """Length of vector""" 38 | return float(np.sqrt(np.sum(self**2))) 39 | 40 | @length.setter 41 | def length(self, value): 42 | if not np.isscalar(value): 43 | raise ValueError('Length must be a scalar') 44 | value = float(value) 45 | if self.length != 0: 46 | new_length = value/self.length 47 | self *= new_length 48 | return 49 | if value != 0: 50 | raise ZeroDivisionError('Cannot resize vector of length 0 to ' 51 | 'nonzero length') 52 | 53 | @property 54 | def rho(self): 55 | """Radial coordinate of this vector (equal to the length of the vector)""" 56 | return self.length 57 | 58 | @rho.setter 59 | def rho(self, value): 60 | self.length = value 61 | 62 | @property 63 | def theta(self): 64 | """Angular coordinate / azimuthal angle of this vector in radians 65 | 66 | Based on polar coordinate space (or sperical coordinate space for `Vector3`) 67 | returns angle between this vector and the positive x-axis 68 | range: (-pi <= theta <= pi) 69 | """ 70 | return float(np.arctan2(self.y, self.x)) 71 | 72 | # pylint: disable=fixme 73 | # TODO: Add `theta` and `theta_deg` setters 74 | # @theta.setter 75 | # def theta(self, value): 76 | # ... 77 | 78 | @property 79 | def theta_deg(self): 80 | """Angular coordinate / azimuthal angle of this vector in degrees 81 | 82 | Based on polar coordinate space (or sperical coordinate space for `Vector3`) 83 | returns angle between this vector and the positive x-axis 84 | range: (-180 <= theta_deg <= 180) 85 | """ 86 | return self.theta * 180 / np.pi 87 | 88 | def as_length(self, value): 89 | """Return a new vector scaled to given length""" 90 | new_vec = self.copy() 91 | new_vec.length = value 92 | return new_vec 93 | 94 | def as_percent(self, value): 95 | """Return a new vector scaled by given decimal percent""" 96 | new_vec = self.copy() 97 | new_vec.length = value * self.length 98 | return new_vec 99 | 100 | def as_unit(self): 101 | """Return a new vector scaled to length 1""" 102 | new_vec = self.copy() 103 | new_vec.normalize() 104 | return new_vec 105 | 106 | def normalize(self): 107 | """Scale the length of a vector to 1 in place""" 108 | self.length = 1 109 | return self 110 | 111 | def dot(self, vec): 112 | """Dot product with another vector""" 113 | if not isinstance(vec, self.__class__): 114 | raise TypeError('Dot product operand must be a vector') 115 | return np.dot(self, vec) 116 | 117 | def cross(self, vec): 118 | """Cross product with another vector""" 119 | if not isinstance(vec, self.__class__): 120 | raise TypeError('Cross product operand must be a vector') 121 | return self.__class__(np.cross(self, vec)) 122 | 123 | def angle(self, vec, unit='rad'): 124 | """Calculate the angle between two Vectors 125 | 126 | unit: unit for returned angle, either 'rad' or 'deg'. Defaults to 'rad' 127 | """ 128 | if not isinstance(vec, self.__class__): 129 | raise TypeError('Angle operand must be of class {}' 130 | .format(self.__class__.__name__)) 131 | if unit not in ['deg', 'rad']: 132 | raise ValueError('Only units of rad or deg are supported') 133 | 134 | denom = self.length * vec.length 135 | if denom == 0: 136 | raise ZeroDivisionError('Cannot calculate angle between ' 137 | 'zero-length vector(s)') 138 | 139 | ang = np.arccos(self.dot(vec) / denom) 140 | if unit == 'deg': 141 | ang = ang * 180 / np.pi 142 | return ang 143 | 144 | def __mul__(self, multiplier): 145 | return self.__class__(self.view(np.ndarray) * multiplier) 146 | 147 | 148 | class Vector3(BaseVector): 149 | """Primitive 3D vector defined from the origin 150 | 151 | New Vector3 can be created with: 152 | - another Vector3 153 | - length-3 array 154 | - x, y, and y values 155 | - no input (returns [0., 0., 0.]) 156 | """ 157 | 158 | # pylint: disable=fixme 159 | # TODO: add support for instantiating Vector3 with `polar`=True 160 | 161 | def __new__(cls, x=None, y=None, z=None): #pylint: disable=arguments-differ 162 | 163 | def read_array(X, Y, Z): 164 | """Build Vector3 from another Vector3, [x, y, z], or x/y/z""" 165 | if isinstance(X, cls) and Y is None and Z is None: 166 | return cls(X.x, X.y, X.z) 167 | if (isinstance(X, (list, tuple, np.ndarray)) and len(X) == 3 and 168 | Y is None and Z is None): 169 | return cls(X[0], X[1], X[2]) 170 | if X is None and Y is None and Z is None: 171 | return cls(0, 0, 0) 172 | if np.isscalar(X) and np.isscalar(Y) and np.isscalar(Z): 173 | xyz = np.r_[X, Y, Z] 174 | xyz = xyz.astype(float) 175 | return xyz.view(cls) 176 | raise ValueError('Invalid input for Vector3 - must be an instance ' 177 | 'of a Vector3, a length-3 array, 3 scalars, or ' 178 | 'nothing for [0., 0., 0.]') 179 | 180 | return read_array(x, y, z) 181 | 182 | def __array_wrap__(self, out_arr, context=None): #pylint: disable=no-self-use, unused-argument 183 | """This is called at the end of ufuncs 184 | 185 | If the output is the wrong shape, return the ndarray view 186 | instead of vector view 187 | """ 188 | if out_arr.shape != (3,): 189 | out_arr = out_arr.view(np.ndarray) 190 | return out_arr 191 | 192 | def __array_finalize__(self, obj): 193 | """This is called when initializing the vector 194 | 195 | If the constructor is used, obj is None. If slicing is 196 | used, obj has the same class as self. In both these cases, 197 | we let things pass. 198 | 199 | If we are viewing another array class as a vector, then obj has 200 | a different class than self. In this case, if the array has 201 | an invalid shape a ValueError is raised 202 | """ 203 | if obj is None or obj.__class__ is Vector3: 204 | return 205 | if self.shape != (3,): 206 | raise ValueError( 207 | 'Invalid array to view as Vector3 - must be length-3 array.' 208 | ) 209 | 210 | @property 211 | def z(self): 212 | """z-component of vector""" 213 | return self[2] 214 | 215 | @z.setter 216 | def z(self, value): 217 | self[2] = value 218 | 219 | @property 220 | def phi(self): 221 | """Polar angle / inclination of this vector in radians 222 | 223 | Based on sperical coordinate space 224 | returns angle between this vector and the positive z-azis 225 | range: (0 <= phi <= pi) 226 | """ 227 | return np.arctan2(np.sqrt(self.x**2 + self.y**2), self.z) 228 | 229 | # pylint: disable=fixme 230 | # TODO: Add `phi` and `phi_deg` setters 231 | # @phi.setter 232 | # def phi(self, value): 233 | # ... 234 | 235 | @property 236 | def phi_deg(self): 237 | """Polar angle / inclination of this vector in degrees 238 | 239 | Based on sperical coordinate space 240 | returns angle between this vector and the positive z-azis 241 | range: (0 <= phi <= pi) 242 | """ 243 | return self.phi * 180 / np.pi 244 | 245 | 246 | class Vector2(BaseVector): 247 | """Primitive 2D vector defined from the origin 248 | 249 | New Vector2 can be created with: 250 | - another Vector2 251 | - length-2 array 252 | - x and y values 253 | - rho and theta, if polar=True; specify unit as 'rad' (default) or 'deg' 254 | - no input (returns [0., 0.]) 255 | """ 256 | 257 | def __new__(cls, x=None, y=None, polar=False, unit='rad'): #pylint: disable=arguments-differ 258 | 259 | def read_array(X, Y): 260 | """Build Vector2 from another Vector2, [x, y], or x/y""" 261 | if isinstance(X, cls) and Y is None: 262 | if polar: 263 | raise ValueError( 264 | 'When copying a Vector2, polar=True is not supported' 265 | ) 266 | return cls(X.x, X.y) 267 | if (isinstance(X, (list, tuple, np.ndarray)) and len(X) == 2 and 268 | Y is None): 269 | return cls(X[0], X[1], polar, unit) 270 | if X is None and Y is None: 271 | return cls(0, 0, polar, unit) 272 | if np.isscalar(X) and np.isscalar(Y): 273 | if polar: 274 | if unit not in ['deg', 'rad']: 275 | raise ValueError( 276 | 'Only units of rad or deg are supported' 277 | ) 278 | if unit == 'deg': 279 | Y = Y / 180 * np.pi 280 | X, Y = X * np.cos(Y), X * np.sin(Y) 281 | xyz = np.r_[X, Y] 282 | xyz = xyz.astype(float) 283 | return xyz.view(cls) 284 | raise ValueError('Invalid input for Vector2 - must be an instance ' 285 | 'of a Vector2, a length-2 array, 2 scalars, or ' 286 | 'nothing for [0., 0.]') 287 | 288 | return read_array(x, y) 289 | 290 | def __array_wrap__(self, out_arr, context=None): #pylint: disable=no-self-use, unused-argument 291 | if out_arr.shape != (2,): 292 | out_arr = out_arr.view(np.ndarray) 293 | return out_arr 294 | 295 | def __array_finalize__(self, obj): 296 | if obj is None or obj.__class__ is Vector2: 297 | return 298 | if self.shape != (2,): 299 | raise ValueError( 300 | 'Invalid array to view as Vector2 - must be length-2 array.' 301 | ) 302 | 303 | def cross(self, vec): 304 | """Cross product with another vector""" 305 | if not isinstance(vec, self.__class__): 306 | raise TypeError('Cross product operand must be a vector') 307 | return Vector3(0, 0, np.asscalar(np.cross(self, vec))) 308 | 309 | 310 | class BaseVectorArray(BaseVector): 311 | """Class to contain basic operations used by all VectorArray classes""" 312 | 313 | @property 314 | def x(self): 315 | """Array of x-component of vectors""" 316 | return self[:, 0] 317 | 318 | @x.setter 319 | def x(self, value): 320 | self[:, 0] = value 321 | 322 | @property 323 | def y(self): 324 | """Array of y-component of vectors""" 325 | return self[:, 1] 326 | 327 | @y.setter 328 | def y(self, value): 329 | self[:, 1] = value 330 | 331 | @property 332 | def nV(self): 333 | """Number of vectors""" 334 | return self.shape[0] 335 | 336 | def normalize(self): 337 | """Scale the length of all vectors to 1 in place""" 338 | self.length = np.ones(self.nV) 339 | return self 340 | 341 | @property 342 | def dims(self): 343 | """Tuple of different dimension names for Vector type""" 344 | raise NotImplementedError('Please use Vector2Array or Vector3Array') 345 | 346 | @property 347 | def length(self): 348 | """Array of vector lengths""" 349 | return np.sqrt(np.sum(self**2, axis=1)).view(np.ndarray) 350 | 351 | @length.setter 352 | def length(self, l): 353 | l = np.array(l) 354 | if self.nV != l.size: 355 | raise ValueError('Length vector must be the same number of ' 356 | 'elements as vector.') 357 | # This case resizes all vectors with nonzero length 358 | if np.all(self.length != 0): 359 | new_length = l/self.length 360 | for dim in self.dims: 361 | setattr(self, dim, new_length*getattr(self, dim)) 362 | return 363 | # This case only applies to single vectors 364 | if self.nV == 1 and l == 0: 365 | assert self.length == 0, \ 366 | 'Nonzero length should be resized in the first case' 367 | for dim in self.dims: 368 | setattr(self, dim, 0.) 369 | return 370 | # This case only applies if vectors with length == 0 371 | # in an array are getting resized to 0 372 | if self.nV > 1 and np.array_equal(self.length.nonzero(), l.nonzero()): #pylint: disable=no-member 373 | new_length = l/[x if x != 0 else 1 for x in self.length] 374 | for dim in self.dims: 375 | setattr(self, dim, new_length*getattr(self, dim)) 376 | return 377 | # Error if length zero array is resized to nonzero value 378 | raise ZeroDivisionError('Cannot resize vector of length 0 to ' 379 | 'nonzero length') 380 | 381 | def dot(self, vec): 382 | """Dot product with another vector""" 383 | if not isinstance(vec, self.__class__): 384 | raise TypeError('Dot product operand must be a VectorArray') 385 | if self.nV != 1 and vec.nV != 1 and self.nV != vec.nV: 386 | raise ValueError('Dot product operands must have the same ' 387 | 'number of elements.') 388 | return np.sum((getattr(self, d)*getattr(vec, d) for d in self.dims), 1) 389 | 390 | def angle(self, vec, unit='rad'): 391 | """Angle method is only for Vectors, not VectorArrays""" 392 | raise NotImplementedError('angle not implemented for VectorArrays') 393 | 394 | 395 | class Vector3Array(BaseVectorArray): 396 | """List of Vector3 397 | 398 | A new Vector3Array can be created with: 399 | - another Vector3Array 400 | - x/y/z lists of equal length 401 | - n x 3 array 402 | - nothing (returns [[0., 0., 0.]]) 403 | """ 404 | 405 | def __new__(cls, x=None, y=None, z=None): #pylint: disable=arguments-differ 406 | 407 | def read_array(X, Y, Z): 408 | """Build Vector3Array from various inputs""" 409 | if isinstance(X, cls) and Y is None and Z is None: 410 | X = np.atleast_2d(X) 411 | return cls(X.x.copy(), X.y.copy(), X.z.copy()) 412 | if isinstance(X, (list, tuple)): 413 | X = np.array(X) 414 | if isinstance(Y, (list, tuple)): 415 | Y = np.array(Y) 416 | if isinstance(Z, (list, tuple)): 417 | Z = np.array(Z) 418 | if isinstance(X, np.ndarray) and Y is None and Z is None: 419 | X = np.squeeze(X) 420 | if X.size == 3: 421 | X = X.flatten() 422 | return cls(X[0], X[1], X[2]) 423 | if len(X.shape) == 2 and X.shape[1] == 3: 424 | return cls( 425 | X[:, 0].copy(), X[:, 1].copy(), X[:, 2].copy() 426 | ) 427 | raise ValueError( 428 | 'Unexpected shape for vector init: {shp}'.format( 429 | shp=X.shape 430 | ) 431 | ) 432 | if np.isscalar(X) and np.isscalar(Y) and np.isscalar(Z): 433 | X, Y, Z = float(X), float(Y), float(Z) 434 | elif not (isinstance(X, type(Y)) and isinstance(X, type(Z))): 435 | raise TypeError('Must be the same types for x, y, and ' 436 | 'z for vector init') 437 | if isinstance(X, np.ndarray): 438 | if not (X.shape == Y.shape and X.shape == Z.shape): 439 | raise ValueError('Must be the same shapes for x, y, ' 440 | 'and z in vector init') 441 | vec_ndarray = np.c_[X, Y, Z] 442 | vec_ndarray = vec_ndarray.astype(float) 443 | return vec_ndarray.view(cls) 444 | if X is None: 445 | X, Y, Z = 0.0, 0.0, 0.0 446 | vec_ndarray = np.r_[X, Y, Z].reshape((1, 3)) 447 | return np.asarray(vec_ndarray).view(cls) 448 | 449 | return read_array(x, y, z) 450 | 451 | def __array_wrap__(self, out_arr, context=None): #pylint: disable=no-self-use, unused-argument 452 | if len(out_arr.shape) != 2 or out_arr.shape[1] != 3: 453 | out_arr = out_arr.view(np.ndarray) 454 | return out_arr 455 | 456 | def __array_finalize__(self, obj): 457 | if obj is None or obj.__class__ is Vector3Array: 458 | return 459 | if len(self.shape) != 2 or self.shape[1] != 3: #pylint: disable=unsubscriptable-object 460 | raise ValueError( 461 | 'Invalid array to view as Vector3Array - must be ' 462 | 'array of shape (*, 3).' 463 | ) 464 | 465 | def __getitem__(self, i): 466 | """Overriding _getitem__ allows coersion to Vector3 or ndarray""" 467 | item_out = super(Vector3Array, self).__getitem__(i) 468 | if np.isscalar(i): 469 | return item_out.view(Vector3) 470 | if isinstance(i, slice): 471 | return item_out 472 | return item_out.view(np.ndarray) 473 | 474 | @property 475 | def z(self): 476 | """Array of z-component of vectors""" 477 | return self[:, 2] 478 | 479 | @z.setter 480 | def z(self, value): 481 | self[:, 2] = value 482 | 483 | @property 484 | def dims(self): 485 | return ('x', 'y', 'z') 486 | 487 | def cross(self, vec): 488 | """Cross product with another Vector3Array""" 489 | if not isinstance(vec, Vector3Array): 490 | raise TypeError('Cross product operand must be a Vector3Array') 491 | if self.nV != 1 and vec.nV != 1 and self.nV != vec.nV: 492 | raise ValueError('Cross product operands must have the same ' 493 | 'number of elements.') 494 | return Vector3Array(np.cross(self, vec)) 495 | 496 | 497 | class Vector2Array(BaseVectorArray): 498 | """List of Vector2 499 | 500 | A new Vector2Array can be created with: 501 | - another Vector2Array 502 | - x/y lists of equal length 503 | - n x 2 array 504 | - nothing (returns [[0., 0.]]) 505 | """ 506 | 507 | def __new__(cls, x=None, y=None): #pylint: disable=arguments-differ 508 | 509 | def read_array(X, Y): 510 | """Build Vector2Array from various inputs""" 511 | if isinstance(X, cls) and Y is None: 512 | X = np.atleast_2d(X) 513 | return cls(X.x.copy(), X.y.copy()) 514 | if isinstance(X, (list, tuple)): 515 | X = np.array(X) 516 | if isinstance(Y, (list, tuple)): 517 | Y = np.array(Y) 518 | if isinstance(X, np.ndarray) and Y is None: 519 | X = np.squeeze(X) 520 | if X.size == 2: 521 | X = X.flatten() 522 | return cls(X[0], X[1]) 523 | if len(X.shape) == 2 and X.shape[1] == 2: 524 | return cls( 525 | X[:, 0].copy(), X[:, 1].copy() 526 | ) 527 | raise ValueError( 528 | 'Unexpected shape for vector init: {shp}'.format( 529 | shp=X.shape 530 | ) 531 | ) 532 | if np.isscalar(X) and np.isscalar(Y): 533 | X, Y = float(X), float(Y) 534 | elif not isinstance(X, type(Y)): 535 | raise TypeError('Must be the same types for x and y ' 536 | 'for vector init') 537 | if isinstance(X, np.ndarray): 538 | if X.shape != Y.shape: 539 | raise ValueError('Must be the same shapes for x and y ' 540 | 'in vector init') 541 | vec_ndarray = np.c_[X, Y] 542 | vec_ndarray = vec_ndarray.astype(float) 543 | return vec_ndarray.view(cls) 544 | if X is None: 545 | X, Y = 0.0, 0.0 546 | vec_ndarray = np.r_[X, Y].reshape((1, 2)) 547 | return np.asarray(vec_ndarray).view(cls) 548 | 549 | return read_array(x, y) 550 | 551 | def __array_wrap__(self, out_arr, context=None): #pylint: disable=no-self-use, unused-argument 552 | if len(out_arr.shape) != 2 or out_arr.shape[1] != 2: 553 | out_arr = out_arr.view(np.ndarray) 554 | return out_arr 555 | 556 | def __array_finalize__(self, obj): 557 | if obj is None or obj.__class__ is Vector2Array: 558 | return 559 | if len(self.shape) != 2 or self.shape[1] != 2: #pylint: disable=unsubscriptable-object 560 | raise ValueError( 561 | 'Invalid array to view as Vector2Array - must be ' 562 | 'array of shape (*, 2).' 563 | ) 564 | 565 | def __getitem__(self, i): 566 | """Overriding _getitem__ allows coercion to Vector2 or ndarray""" 567 | item_out = super(Vector2Array, self).__getitem__(i) 568 | if np.isscalar(i): 569 | return item_out.view(Vector2) 570 | if isinstance(i, slice): 571 | return item_out 572 | return item_out.view(np.ndarray) 573 | 574 | @property 575 | def dims(self): 576 | return ('x', 'y') 577 | --------------------------------------------------------------------------------