diff --git a/.gitignore b/.gitignore index 4cabce2..1c2a954 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ test/ *pleiszenburg* _build/ MATERIAL/ +.hypothesis/ +.coverage +.pytest_cache/ +htmlcov/ diff --git a/CHANGES.md b/CHANGES.md index 5421d99..270e957 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,32 @@ # Changes +## 0.0.6 (2021-11-07) + +Highlights: Major overhaul of linear algebra functionality, better package structure and a test suite. + +- FEATURE: All vector and vector array classes expose `ndim`, number of dimensions. +- FEATURE: Common base class, `Vector`, for all vector classes. +- FEATURE: Common base class, `VectorArray`, for all vector array classes. +- FEATURE: Vector arrays are iterators. +- FEATURE: Added missing right-hand-side operators to `Vector` and `VectorArray` classes. +- FEATURE: Tuple export of `VectorArray` types can optionally provide direct access to underlying ``ndarray``s, i.e. new ``copy`` parameter can be set to ``False``. +- FEATURE: 3D vectors and vector arrays can export geographic coordinates. +- FEATURE: The `Color` class, using RGBA internally, can now import HSV values. +- FEATURE: Added equality check, "is close" check, tuple export and copy to `Matrix`. +- FEATURE: Added new `MatrixArray` class. +- FEATURE: New dedicated sub-module for core animation engine named `bewegung.animation`. +- FEATURE: New dedicated sub-module for `DrawingBoard` named `bewegung.drawingboard`, now allowing direct import. +- FEATURE: New dedicated sub-module for linear algebra named `bewegung.lingalg`. +- FEATURE: All linear algebra classes have consistent dtype and error handling. +- FEATURE: Cleanup of internal type hierarchy. +- FEATURE: Added test suite with some initial tests, based on `pytest`, `hypothesis` and `coverage`. +- API CHANGE: Vector array method `update_from_vector` renamed to `update_from_vectorarray`. +- API CHANGE: `Vector2Ddist` and `VectorArray2Ddist` removed in favor of meta data dictionaries within all vector, vector array, matrix and matrix array classes. +- FIX: Development dependency switched from unmaintained `python-language-server` to maintained fork `python-lsp-server`. +- FIX: Imports in `contrib` were broken. +- FIX: `test` target in `makefile` was broken. +- FIX: `typeguard` was not really an optional dependency. + ## 0.0.5 (2021-07-30) - FEATURE: Python 3.9 support. diff --git a/README.md b/README.md index 33f97b2..8afcdb1 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,16 @@ `bewegung` can be installed both via ``conda`` and via ``pip``. +### Via `conda` + +An almost complete installation can be triggered by running: + +```bash +conda install -c conda-forge bewegung +``` + +Please note that [mplcairo](https://github.com/matplotlib/mplcairo), a dependency of `bewegung` and alternative backend for `matplotlib`, is currently not available via `conda` and must be installed manually. `bewegung` [does also work without `mplcairo` present](https://bewegung.readthedocs.io/en/latest/canvas.html#acceleratingmatplotlib) and falls back to the `cairo` backend of `matplotlib`. + ### Via `pip` A bare **minimum** of `bewegung` can be installed with Python's package manager `pip`: @@ -37,16 +47,6 @@ pip install -vU bewegung[all] Certain non-Python **prerequisites** must installed separately and before invoking the above command. [For detailed instructions, see documentation](https://bewegung.readthedocs.io/en/latest/installation.html). Most notably, `ffmpeg` should be installed for producing actual video files instead of video frames as individual files. See [download section](https://ffmpeg.org/download.html) of the `ffmpeg` project website for further instructions. -### Via `conda` - -An almost complete installation can be triggered by running: - -```bash -conda install -c conda-forge bewegung -``` - -Please note that [mplcairo](https://github.com/matplotlib/mplcairo), a dependency of `bewegung` and alternative backend for `matplotlib`, is currently not available via `conda` and must be installed manually. `bewegung` [does also work without `mplcairo` present](https://bewegung.readthedocs.io/en/latest/canvas.html#acceleratingmatplotlib) and falls back to the `cairo` backend of `matplotlib`. - ## Example See [`demo.py`](https://github.com/pleiszenburg/bewegung/blob/master/demo/demo.py). @@ -65,6 +65,6 @@ This resulting `video.mp4` file should look like this: See [documentation](https://bewegung.readthedocs.io). -`bewegung`'s development status is "well-tested alpha". Its API should not be considered stable until the project is labeled "beta" or better, although significant changes are very unlikely. +`bewegung`'s development status is "well-tested alpha". Its API should not be considered stable until the project is labeled "beta" or better. `bewegung` can be drastically accelerated by deactivating debugging features. See [relevant section in the documentation](https://bewegung.readthedocs.io/en/latest/performance.html#typecheckingperformance). diff --git a/demo/demo.py b/demo/demo.py index 7794af3..6e31b0b 100644 --- a/demo/demo.py +++ b/demo/demo.py @@ -42,10 +42,9 @@ Camera, Color, Video, Vector2D, Vector3D, VectorArray3D, FadeInEffect, FadeOutEffect, - backends, ) -DrawingBoard = backends['drawingboard'].type +from bewegung.drawingboard import DrawingBoard # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CONST @@ -145,9 +144,9 @@ def project(self): for line2d in self._lines2d for a, b in zip(line2d[:-1], line2d[1:]) ] - self._lines2d.sort(key = lambda item: item[0].dist, reverse = True) - minimum = self._lines2d[0][0].dist - maximum = self._lines2d[-1][0].dist + self._lines2d.sort(key = lambda item: item[0].meta['dist'], reverse = True) + minimum = self._lines2d[0][0].meta['dist'] + maximum = self._lines2d[-1][0].meta['dist'] self._factor = lambda x: (x - minimum) / (maximum - minimum) @FadeInEffect(v.time_from_seconds(4.0)) @@ -158,7 +157,7 @@ def project(self): ) def wiremesh(self, canvas): for line in self._lines2d: - factor = self._factor(line[0].dist) + factor = self._factor(line[0].meta['dist']) gray = round(64 + 191 * factor) canvas.draw_polygon( *line, diff --git a/demo/demo_mpl.py b/demo/demo_mpl.py index 92ee550..7da14bd 100644 --- a/demo/demo_mpl.py +++ b/demo/demo_mpl.py @@ -42,10 +42,9 @@ Color, Video, Vector2D, FadeInEffect, FadeOutEffect, - backends, ) -DrawingBoard = backends['drawingboard'].type +from bewegung.drawingboard import DrawingBoard # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CONST diff --git a/demo/demo_mpl_fast.py b/demo/demo_mpl_fast.py index 845e5aa..1e58beb 100644 --- a/demo/demo_mpl_fast.py +++ b/demo/demo_mpl_fast.py @@ -42,10 +42,9 @@ Color, Video, Vector2D, FadeInEffect, FadeOutEffect, - backends, ) -DrawingBoard = backends['drawingboard'].type +from bewegung.drawingboard import DrawingBoard # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CONST diff --git a/docs/algebra.rst b/docs/algebra.rst index 2799e3e..1a2e6f3 100644 --- a/docs/algebra.rst +++ b/docs/algebra.rst @@ -1,7 +1,7 @@ Linear Algebra ============== -``bewegung`` offers a bunch of convenience classes for tasks related to linear algebra. +``bewegung`` offers a number of *convenience* classes for tasks related to linear algebra. They are isolated into a distinct sub-module named ``bewegung.linalg``. .. note:: @@ -12,4 +12,5 @@ Linear Algebra :caption: The API in detail vectors + matrices camera diff --git a/docs/camera.rst b/docs/camera.rst index 188d4d2..82dad31 100644 --- a/docs/camera.rst +++ b/docs/camera.rst @@ -1,7 +1,7 @@ 3D to 2D projections: A Camera ============================== -``bewegung`` includes a `pin-hole camera`_ for simple 3D to 2D projections. In a nutshell, the a :class:`bewegung.Camera` object can convert a :class:`bewegung.Vector3D` object into a :class:`bewegung.Vector2Ddist` object given a location and direction in 3D space, i.e. the 3D vector is projected into a plane in 2D space. Because the "camera" is actually not a rendering system on its own, it simply adds meta information to the returned 2D vector: The absolute distance from the "pinhole" in 3D space to the vector in 3D space. This allows to (manually) implement various kinds of depth perception, e.g. backgrounds and foregrounds, in visualizations. The camera is a useful tool if e.g. multiple :ref:`drawing backends ` are combined within a single animation and some kind of common 3D visualization is required. A typical combination is :ref:`datashader ` for density distributions and :ref:`cairo ` for annotations on top, see :ref:`gallery ` for examples. +``bewegung`` includes a `pin-hole camera`_ for simple 3D to 2D projections. In a nutshell, the a :class:`bewegung.Camera` object can convert a :class:`bewegung.Vector3D` object into a :class:`bewegung.Vector2D` object given a location and direction in 3D space, i.e. the 3D vector is projected into a plane in 2D space. Because the "camera" is actually not a rendering system on its own, it simply adds meta data (:attr:`bewegung.Vector.meta`) to the returned 2D vector: The absolute distance (``meta["dist"]``) from the "pinhole" in 3D space to the vector in 3D space. This allows to (manually) implement various kinds of depth perception, e.g. backgrounds and foregrounds, in visualizations. The camera is a useful tool if e.g. multiple :ref:`drawing backends ` are combined within a single animation and some kind of common 3D visualization is required. A typical combination is :ref:`datashader ` for density distributions and :ref:`cairo ` for annotations on top, see :ref:`gallery ` for examples. .. _pin-hole camera: https://en.wikipedia.org/wiki/Pinhole_camera diff --git a/docs/drawingboard.rst b/docs/drawingboard.rst index 460cca1..ea90a99 100644 --- a/docs/drawingboard.rst +++ b/docs/drawingboard.rst @@ -41,10 +41,10 @@ The ``DrawingBoard`` Class The ``DrawingBoard`` class makes use of :ref:`vectors ` and :ref:`colors `. -.. autoclass:: bewegung.core.backends.drawingboard.core.DrawingBoard +.. autoclass:: bewegung.drawingboard.DrawingBoard :members: :private-members: .. note:: - The ``DrawingBoard`` class can, most efficiently, be accessed via ``bewegung.backends['drawingboard'].type``. + The ``DrawingBoard`` class can be imported from ``bewegung.drawingboard``. diff --git a/docs/index.rst b/docs/index.rst index b6a4f24..b85c648 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,7 +39,7 @@ bewegung - a versatile video renderer .. warning:: - ``bewegung``'s development status is "**well-tested alpha**". Its API should not be considered stable until the project is labeled "beta" or better, although significant changes are very unlikely. + ``bewegung``'s development status is "**well-tested alpha**". Its API should not be considered stable until the project is labeled "beta" or better. .. toctree:: :maxdepth: 2 diff --git a/docs/installation.rst b/docs/installation.rst index e3aa1fb..a5dad87 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -17,6 +17,21 @@ Quick Install Guide `bewegung` can be installed both via ``conda`` and via ``pip``. +Via ``conda`` +~~~~~~~~~~~~~ + +An almost complete installation can be triggered by running: + +.. code:: bash + + conda install -c conda-forge bewegung + +.. note:: + + `mplcairo`_, a dependency of ``bewegung`` and alternative backend for ``matplotlib``, is currently not available via ``conda`` and must be installed manually. ``bewegung`` :ref:`does also work without mplcairo present ` and falls back to the ``cairo`` backend of ``matplotlib``. + +.. _mplcairo: https://github.com/matplotlib/mplcairo + Via ``pip`` ~~~~~~~~~~~ @@ -59,21 +74,6 @@ The actual installation of ``bewegung`` can now be triggered as follows: pip install -vU bewegung[all] # install bewegung -Via ``conda`` -~~~~~~~~~~~~~ - -An almost complete installation can be triggered by running: - -.. code:: bash - - conda install -c conda-forge bewegung - -.. note:: - - `mplcairo`_, a dependency of ``bewegung`` and alternative backend for ``matplotlib``, is currently not available via ``conda`` and must be installed manually. ``bewegung`` :ref:`does also work without mplcairo present ` and falls back to the ``cairo`` backend of ``matplotlib``. - -.. _mplcairo: https://github.com/matplotlib/mplcairo - Validate Installation ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/layer_tasks.rst b/docs/layer_tasks.rst index fe9dc14..710a8af 100644 --- a/docs/layer_tasks.rst +++ b/docs/layer_tasks.rst @@ -23,6 +23,6 @@ The ``Layer`` Class Do not work with this class directly. Use the :meth:`bewegung.Video.layer` method instead. -.. autoclass:: bewegung.core.layer.Layer +.. autoclass:: bewegung.animation.Layer :members: :private-members: diff --git a/docs/matrices.rst b/docs/matrices.rst new file mode 100644 index 0000000..d281f96 --- /dev/null +++ b/docs/matrices.rst @@ -0,0 +1,22 @@ +.. _matrices: + +Matrices and Matrix Arrays +========================== + +``bewegung`` offers :ref:`matrices ` and :ref:`matrix arrays `. Both of them work for 2x2 and 3x3 shapes. They are intended for simple tasks like rotations of :ref:`vectors ` and :ref:`vector arrays `. + +.. _matrix_single: + +The ``Matrix`` Class +-------------------- + +.. autoclass:: bewegung.Matrix + :members: + +.. _matrix_array: + +The ``MatrixArray`` Class +------------------------- + +.. autoclass:: bewegung.MatrixArray + :members: diff --git a/docs/sequences.rst b/docs/sequences.rst index 800d3c0..e698d5b 100644 --- a/docs/sequences.rst +++ b/docs/sequences.rst @@ -5,7 +5,7 @@ Sequences A *sequence* is a *time-span within a video*. A sequence can hold multiple :ref:`prepare tasks ` and :ref:`layer tasks `. Every prepare task and layer task is evaluated once per video frame, if the video frames is *within the temporal boundaries* of the sequence. -From a practical point of view, sequences are special user-defined and decorated classes. A user must not instantiate a sequence class. Prepare tasks and layer tasks are special, also decorated methods within those user-defined classes. If a constructor method is present in the user-defined sequence class, it is evaluated once per video rendering run (:meth:`bewegung.Video.render`). It is also (re-) evaluated if the video object is reset (:meth:`bewegung.Video.reset`). User-defined sequence classes are eventually "mixed" with the :class:`bewegung.core.sequence.Sequence` class. All of its methods and properties are therefore available in user-defined sequence classes. From a technical point of view, new classes are generated by making :class:`bewegung.core.sequence.Sequence` inherit from user-defined sequence classes. +From a practical point of view, sequences are special user-defined and decorated classes. A user must not instantiate a sequence class. Prepare tasks and layer tasks are special, also decorated methods within those user-defined classes. If a constructor method is present in the user-defined sequence class, it is evaluated once per video rendering run (:meth:`bewegung.Video.render`). It is also (re-) evaluated if the video object is reset (:meth:`bewegung.Video.reset`). User-defined sequence classes are eventually "mixed" with the :class:`bewegung.animation.Sequence` class. All of its methods and properties are therefore available in user-defined sequence classes. From a technical point of view, new classes are generated by making :class:`bewegung.core.sequence.Sequence` inherit from user-defined sequence classes. The ``Video.sequence`` Decorator -------------------------------- @@ -19,5 +19,5 @@ The ``Sequence`` Class Do not work with this class directly. Use the :meth:`bewegung.Video.sequence` method instead. -.. autoclass:: bewegung.core.sequence.Sequence +.. autoclass:: bewegung.animation.Sequence :members: diff --git a/docs/vectors.rst b/docs/vectors.rst index 7bfcd42..f95ac2b 100644 --- a/docs/vectors.rst +++ b/docs/vectors.rst @@ -3,29 +3,29 @@ Vectors and Vector Arrays ========================= -``bewegung`` offers :ref:`vectors ` and :ref:`vector arrays `. Both of them are available in 2D and 3D variants. In 2D space, there are additional variants exposing a "distance property". The distance can be used to describe a (relative) distance to a camera or observer, which is useful for various types of renderings. Finally, there is also a :ref:`matrix ` for simple tasks like rotations both in 2D and 3D space. +``bewegung`` offers :ref:`vectors ` and :ref:`vector arrays `. Both of them are available in 2D and 3D variants. Both vectors and vector arrays can interact with each other as well as with :ref:`matrices `. .. note:: - Besides simple vector algebra, a lot of ``bewegung``'s functions and methods expect geometric input using vector classes. + Besides simple vector algebra, a lot of ``bewegung``'s functions and methods expect geometric input using vector objects. .. _vector_single: Vector Classes -------------- -The vector classes describe individual vectors in 2D and 3D space. Vectors are "statically typed", use Python number types and can either have ``int`` or ``float`` components. The data type of a vector is exposed through its ``dtype`` property. +The vector classes describe individual vectors in 2D and 3D space. Vectors are "statically typed", i.e. all components are of one single type, and use Python number types (sub-classes of ``numbers.Number``). The data type of a vector is exposed through its ``dtype`` property. -The ``Vector2D`` class -~~~~~~~~~~~~~~~~~~~~~~ +The ``Vector`` base class +~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: bewegung.Vector2D +.. autoclass:: bewegung.Vector :members: -The ``Vector2Ddist`` class -~~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``Vector2D`` class +~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: bewegung.Vector2Ddist +.. autoclass:: bewegung.Vector2D :members: The ``Vector3D`` class @@ -41,16 +41,16 @@ Vector Array Classes The vector array classes describe arrays of individual vectors in 2D and 3D space. Vector arrays are "statically typed" and use ``numpy`` arrays for storing data. Just like ``numpy.ndarray`` objects, they expose a ``dtype`` property. -The ``VectorArray2D`` class -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``VectorArray`` base class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: bewegung.VectorArray2D +.. autoclass:: bewegung.VectorArray :members: -The ``VectorArray2Ddist`` class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``VectorArray2D`` class +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: bewegung.VectorArray2Ddist +.. autoclass:: bewegung.VectorArray2D :members: The ``VectorArray3D`` class @@ -58,11 +58,3 @@ The ``VectorArray3D`` class .. autoclass:: bewegung.VectorArray3D :members: - -.. _matrix: - -The ``Matrix`` Class --------------------- - -.. autoclass:: bewegung.Matrix - :members: diff --git a/makefile b/makefile index c2bbb88..2d60287 100644 --- a/makefile +++ b/makefile @@ -5,10 +5,10 @@ black: clean: -rm -r build/* -(cd docs/; make clean) - find src/ docs/ -name '*.pyc' -exec rm -f {} + - find src/ docs/ -name '*.pyo' -exec rm -f {} + - find src/ docs/ -name '*~' -exec rm -f {} + - find src/ docs/ -name '__pycache__' -exec rm -fr {} + + find src/ docs/ tests/ -name '*.pyc' -exec rm -f {} + + find src/ docs/ tests/ -name '*.pyo' -exec rm -f {} + + find src/ docs/ tests/ -name '*~' -exec rm -f {} + + find src/ docs/ tests/ -name '__pycache__' -exec rm -fr {} + find src/ -name '*.htm' -exec rm -f {} + find src/ -name '*.html' -exec rm -f {} + find src/ -name '*.so' -exec rm -f {} + @@ -16,6 +16,9 @@ clean: -rm -r dist/* -rm -r src/*.egg-info +demo: + python demo/demo.py + docs: @(cd docs; make clean; make html) @@ -36,8 +39,12 @@ upload: done test: - -rm -r frames/ - mkdir frames - python demo.py + make docs + make test_quick + +test_quick: + make clean + HYPOTHESIS_PROFILE=dev pytest --cov=bewegung --cov-config=setup.cfg --hypothesis-show-statistics # --capture=no + coverage html -.PHONY: clean docs release test +.PHONY: clean demo docs release test diff --git a/setup.cfg b/setup.cfg index f48fdad..e93a065 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,14 @@ [metadata] description-file = README.md license_file = LICENSE + +[tool:pytest] +testpaths = tests + +[coverage:run] +branch = True +parallel = True + +[coverage:paths] +source = + src/ diff --git a/setup.py b/setup.py index 1dfa1b0..b24ef13 100644 --- a/setup.py +++ b/setup.py @@ -29,11 +29,12 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +import os + from setuptools import ( find_packages, setup, ) -import os from docs.version import get_version @@ -62,8 +63,10 @@ # Requirements extras_require = { "dev": [ + "coverage", "black", - "python-language-server[all]", + "hypothesis", + "python-lsp-server[all]", # superseeding python-language-server "psutil", "setuptools", "Sphinx", @@ -71,6 +74,8 @@ "sphinx-rtd-theme", "sphinxembeddedvideos", # https://github.com/sphinx-contrib/youtube/issues/9#issuecomment-734295832 "myst-parser", # markdown in sphinx + "pytest", + "pytest-cov", "twine", "wheel", ], @@ -106,9 +111,11 @@ "typeguard", # for type checking (optional) ], } -extras_require["all"] = list( - {rq for target in extras_require.keys() for rq in extras_require[target]} -) +extras_require["all"] = list({ + rq + for target in extras_require.values() + for rq in target +}) extras_require["docs"] = list(set(extras_require["all"]) - {'PyGObject'}) # Install package diff --git a/src/bewegung/__init__.py b/src/bewegung/__init__.py index c458770..5942392 100644 --- a/src/bewegung/__init__.py +++ b/src/bewegung/__init__.py @@ -28,19 +28,8 @@ # EXPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -__version__ = '0.0.5' - -from .core.camera import Camera -from .core.backends import * -from .core.color import Color -from .core.const import * -from .core.effects import EffectBase, FadeInEffect, FadeOutEffect -from .core.encoders import EncoderBase, FFmpegH264Encoder, FFmpegGifEncoder -from .core.indexpool import IndexPool -from .core.layer import Layer -from .core.sequence import Sequence -from .core.task import Task -from .core.time import Time -from .core.timescale import TimeScale -from .core.vector import * -from .core.video import Video +__version__ = '0.0.6' + +from .animation import * +from .lib import * +from .linalg import * diff --git a/src/bewegung/animation/__init__.py b/src/bewegung/animation/__init__.py new file mode 100644 index 0000000..32398a2 --- /dev/null +++ b/src/bewegung/animation/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/animation/__init__.py: Animation engine + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# EXPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from ._backends import * +from ._const import * +from ._effects import EffectBase, FadeInEffect, FadeOutEffect +from ._encoders import EncoderBase, FFmpegH264Encoder, FFmpegGifEncoder +from ._indexpool import IndexPool +from ._layer import Layer +from ._sequence import Sequence +from ._task import Task +from ._time import Time +from ._timescale import TimeScale +from ._video import Video diff --git a/src/bewegung/animation/_abc.py b/src/bewegung/animation/_abc.py new file mode 100644 index 0000000..3b3f298 --- /dev/null +++ b/src/bewegung/animation/_abc.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/animation/_abc.py: Abstract base classes + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from abc import ABC + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASSES +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class BackendABC(ABC): + pass + +class EffectABC(ABC): + pass + +class EncoderABC(ABC): + pass + +class IndexPoolABC(ABC): + pass + +class LayerABC(ABC): + pass + +class SequenceABC(ABC): + pass + +class TaskABC(ABC): + pass + +class TimeABC(ABC): + pass + +class TimeScaleABC(ABC): + pass + +class VideoABC(ABC): + pass diff --git a/src/bewegung/core/backends/__init__.py b/src/bewegung/animation/_backends/__init__.py similarity index 92% rename from src/bewegung/core/backends/__init__.py rename to src/bewegung/animation/_backends/__init__.py index 0ebe442..15673ee 100644 --- a/src/bewegung/core/backends/__init__.py +++ b/src/bewegung/animation/_backends/__init__.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/__init__.py: Collects backend types for layers + src/bewegung/animation/_backends/__init__.py: Collects backend types for layers Copyright (C) 2020-2021 Sebastian M. Ernst diff --git a/src/bewegung/core/backends/_base.py b/src/bewegung/animation/_backends/_base.py similarity index 97% rename from src/bewegung/core/backends/_base.py rename to src/bewegung/animation/_backends/_base.py index 8641ccb..2dbfa13 100644 --- a/src/bewegung/core/backends/_base.py +++ b/src/bewegung/animation/_backends/_base.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/_base.py: Backend base class + src/bewegung/animation/_backends/_base.py: Backend base class Copyright (C) 2020-2021 Sebastian M. Ernst @@ -32,8 +32,8 @@ from PIL.Image import Image -from ..abc import BackendABC, VideoABC -from ..typeguard import typechecked +from ...lib import typechecked +from .._abc import BackendABC, VideoABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/bewegung/core/backends/_load.py b/src/bewegung/animation/_backends/_load.py similarity index 91% rename from src/bewegung/core/backends/_load.py rename to src/bewegung/animation/_backends/_load.py index be6d691..1423dbf 100644 --- a/src/bewegung/core/backends/_load.py +++ b/src/bewegung/animation/_backends/_load.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/_load.py: Backend inventory and loader + src/bewegung/animation/_backends/_load.py: Backend inventory and loader Copyright (C) 2020-2021 Sebastian M. Ernst @@ -33,7 +33,7 @@ import importlib import os -from ..typeguard import typechecked +from ...lib import typechecked # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -57,7 +57,7 @@ def __init__(self): ] for name in backend_modules: - self[name] = importlib.import_module(f'bewegung.core.backends.{name:s}').Backend() + self[name] = importlib.import_module(f'bewegung.animation._backends.{name:s}').Backend() def isinstance(self, obj: Any) -> bool: diff --git a/src/bewegung/core/backends/cairo.py b/src/bewegung/animation/_backends/cairo.py similarity index 95% rename from src/bewegung/core/backends/cairo.py rename to src/bewegung/animation/_backends/cairo.py index f73a684..8feb722 100644 --- a/src/bewegung/core/backends/cairo.py +++ b/src/bewegung/animation/_backends/cairo.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/cairo.py: Cairo backend + src/bewegung/animation/_backends/cairo.py: Cairo backend Copyright (C) 2020-2021 Sebastian M. Ernst @@ -32,9 +32,9 @@ from PIL.Image import Image, frombuffer, merge +from ...lib import typechecked +from .._abc import VideoABC from ._base import BackendBase -from ..abc import VideoABC -from ..typeguard import typechecked # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/bewegung/core/backends/datashader.py b/src/bewegung/animation/_backends/datashader.py similarity index 95% rename from src/bewegung/core/backends/datashader.py rename to src/bewegung/animation/_backends/datashader.py index bc7703e..3ef788d 100644 --- a/src/bewegung/core/backends/datashader.py +++ b/src/bewegung/animation/_backends/datashader.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/datashader.py: Datashader backend + src/bewegung/animation/_backends/datashader.py: Datashader backend Copyright (C) 2020-2021 Sebastian M. Ernst @@ -33,9 +33,9 @@ from PIL.Image import Image from PIL import ImageOps +from ...lib import typechecked +from .._abc import VideoABC from ._base import BackendBase -from ..abc import VideoABC -from ..typeguard import typechecked # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/bewegung/core/backends/drawingboard/backend.py b/src/bewegung/animation/_backends/drawingboard.py similarity index 89% rename from src/bewegung/core/backends/drawingboard/backend.py rename to src/bewegung/animation/_backends/drawingboard.py index ef78cb3..d291bc5 100644 --- a/src/bewegung/core/backends/drawingboard/backend.py +++ b/src/bewegung/animation/_backends/drawingboard.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/drawingboard/backend.py: Simple 2D cairo renderer + src/bewegung/animation/_backends/drawingboard.py: Simple 2D cairo renderer Copyright (C) 2020-2021 Sebastian M. Ernst @@ -32,9 +32,9 @@ from PIL.Image import Image -from .._base import BackendBase -from ...abc import VideoABC -from ...typeguard import typechecked +from ...lib import typechecked +from .._abc import VideoABC +from ._base import BackendBase # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -56,7 +56,7 @@ def _prototype(self, video: VideoABC, **kwargs) -> Callable: def _load(self): - from .core import DrawingBoard + from ...drawingboard import DrawingBoard self._type = DrawingBoard diff --git a/src/bewegung/core/backends/matplotlib.py b/src/bewegung/animation/_backends/matplotlib.py similarity index 93% rename from src/bewegung/core/backends/matplotlib.py rename to src/bewegung/animation/_backends/matplotlib.py index 3663e39..a4f39fa 100644 --- a/src/bewegung/core/backends/matplotlib.py +++ b/src/bewegung/animation/_backends/matplotlib.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/matplotlib.py: Matplotlib backend + src/bewegung/animation/_backends/matplotlib.py: Matplotlib backend Copyright (C) 2020-2021 Sebastian M. Ernst @@ -28,14 +28,15 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +from numbers import Number from typing import Any, Callable import warnings from PIL.Image import Image, fromarray, frombuffer, merge +from ...lib import Color, typechecked +from .._abc import VideoABC from ._base import BackendBase -from ..abc import ColorABC, NumberTypes, VideoABC -from ..typeguard import typechecked # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -63,8 +64,8 @@ def _prototype(self, video: VideoABC, **kwargs) -> Callable: kwargs['width'] = video.width if 'height' not in kwargs.keys(): kwargs['height'] = video.height - assert isinstance(kwargs['width'], NumberTypes) - assert isinstance(kwargs['height'], NumberTypes) + assert isinstance(kwargs['width'], Number) + assert isinstance(kwargs['height'], Number) kwargs['figsize'] = ( kwargs.pop('width') / kwargs['dpi'], kwargs.pop('height') / kwargs['dpi'], @@ -73,7 +74,7 @@ def _prototype(self, video: VideoABC, **kwargs) -> Callable: if 'background_color' in kwargs.keys() and 'facecolor' in kwargs.keys(): kwargs.pop('background_color') if 'background_color' in kwargs.keys(): - if not isinstance(kwargs['background_color'], ColorABC): + if not isinstance(kwargs['background_color'], Color): raise TypeError('color expected') kwargs['facecolor'] = f'#{kwargs.pop("background_color").as_hex():s}' if 'facecolor' not in kwargs.keys(): diff --git a/src/bewegung/core/backends/pillow.py b/src/bewegung/animation/_backends/pillow.py similarity index 92% rename from src/bewegung/core/backends/pillow.py rename to src/bewegung/animation/_backends/pillow.py index 13d6408..8a676e1 100644 --- a/src/bewegung/core/backends/pillow.py +++ b/src/bewegung/animation/_backends/pillow.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/pillow.py: Pillow backend + src/bewegung/animation/_backends/pillow.py: Pillow backend Copyright (C) 2020-2021 Sebastian M. Ernst @@ -32,9 +32,9 @@ from PIL.Image import Image, new +from ...lib import Color, typechecked +from .._abc import VideoABC from ._base import BackendBase -from ..abc import ColorABC, VideoABC -from ..typeguard import typechecked # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -64,7 +64,7 @@ def _prototype(self, video: VideoABC, **kwargs) -> Callable: if 'color' in kwargs.keys() and 'background_color' in kwargs.keys(): kwargs.pop('background_color') if 'background_color' in kwargs.keys(): - if not isinstance(kwargs['background_color'], ColorABC): + if not isinstance(kwargs['background_color'], Color): raise TypeError('color expected') kwargs['color'] = kwargs.pop("background_color").as_rgba_int() diff --git a/src/bewegung/core/const.py b/src/bewegung/animation/_const.py similarity index 94% rename from src/bewegung/core/const.py rename to src/bewegung/animation/_const.py index f75a7f5..ba93a7b 100644 --- a/src/bewegung/core/const.py +++ b/src/bewegung/animation/_const.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/const.py: Const values + src/bewegung/animation/_const.py: Const values Copyright (C) 2020-2021 Sebastian M. Ernst @@ -29,7 +29,6 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ FPS_DEFAULT = 60 -FLOAT_DEFAULT = 'f4' FFMPEG_CRF_DEFAULT = 17 FFMPEG_PRESET_DEFAULT = "slow" diff --git a/src/bewegung/core/effects.py b/src/bewegung/animation/_effects.py similarity index 97% rename from src/bewegung/core/effects.py rename to src/bewegung/animation/_effects.py index 48be18e..96025d6 100644 --- a/src/bewegung/core/effects.py +++ b/src/bewegung/animation/_effects.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/effects.py: Video frame effects + src/bewegung/animation/_effects.py: Video frame effects Copyright (C) 2020-2021 Sebastian M. Ernst @@ -32,8 +32,8 @@ from PIL import Image as PIL_Image -from .abc import EffectABC, LayerABC, SequenceABC, TimeABC, VideoABC -from .typeguard import typechecked +from ..lib import typechecked +from ._abc import EffectABC, LayerABC, SequenceABC, TimeABC, VideoABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS: BASE diff --git a/src/bewegung/core/encoders.py b/src/bewegung/animation/_encoders.py similarity index 98% rename from src/bewegung/core/encoders.py rename to src/bewegung/animation/_encoders.py index a4687ca..1e275fa 100644 --- a/src/bewegung/core/encoders.py +++ b/src/bewegung/animation/_encoders.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/encoders.py: Wrapper for video encoders + src/bewegung/animation/_encoders.py: Wrapper for video encoders Copyright (C) 2020-2021 Sebastian M. Ernst @@ -32,14 +32,14 @@ from typing import BinaryIO, Union, Type from subprocess import Popen, PIPE, DEVNULL -from .abc import EncoderABC, VideoABC -from .const import ( +from ..lib import typechecked +from ._abc import EncoderABC, VideoABC +from ._const import ( PIPE_BUFFER_DEFAULT, FFMPEG_CRF_DEFAULT, FFMPEG_PRESET_DEFAULT, FFPMEG_TUNE_DEFAULT, ) -from .typeguard import typechecked # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS: BASE diff --git a/src/bewegung/core/indexpool.py b/src/bewegung/animation/_indexpool.py similarity index 96% rename from src/bewegung/core/indexpool.py rename to src/bewegung/animation/_indexpool.py index 692a7fb..2ec832e 100644 --- a/src/bewegung/core/indexpool.py +++ b/src/bewegung/animation/_indexpool.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/indexpool.py: Pools of unique index values + src/bewegung/animation/_indexpool.py: Pools of unique index values Copyright (C) 2020-2021 Sebastian M. Ernst @@ -30,8 +30,8 @@ from typing import List -from .abc import IndexPoolABC -from .typeguard import typechecked +from ..lib import typechecked +from ._abc import IndexPoolABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/bewegung/core/layer.py b/src/bewegung/animation/_layer.py similarity index 95% rename from src/bewegung/core/layer.py rename to src/bewegung/animation/_layer.py index 92ebe71..a4304f1 100644 --- a/src/bewegung/core/layer.py +++ b/src/bewegung/animation/_layer.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/layer.py: Layer function/method wrapper + src/bewegung/animation/_layer.py: Layer function/method wrapper Copyright (C) 2020-2021 Sebastian M. Ernst @@ -33,10 +33,10 @@ from PIL import Image as PIL_Image -from .abc import EffectABC, LayerABC, SequenceABC, TimeABC, VideoABC, Vector2DABC -from .backends import backends -from .typeguard import typechecked -from .vector import Vector2D +from ..lib import typechecked +from ..linalg import Vector2D +from ._abc import EffectABC, LayerABC, SequenceABC, TimeABC, VideoABC +from ._backends import backends # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -65,7 +65,7 @@ def __init__(self, zindex: int, video: VideoABC, canvas: Union[Callable, None] = None, - offset: Union[Vector2DABC, None] = None, + offset: Union[Vector2D, None] = None, ): # consistency checks are performed in Video.layer diff --git a/src/bewegung/core/sequence.py b/src/bewegung/animation/_sequence.py similarity index 96% rename from src/bewegung/core/sequence.py rename to src/bewegung/animation/_sequence.py index 3896392..f225479 100644 --- a/src/bewegung/core/sequence.py +++ b/src/bewegung/animation/_sequence.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/sequence.py: Video Sequence + src/bewegung/animation/_sequence.py: Video Sequence Copyright (C) 2020-2021 Sebastian M. Ernst @@ -30,8 +30,8 @@ from typing import Dict -from .abc import SequenceABC, TimeABC, VideoABC -from .typeguard import typechecked +from ..lib import typechecked +from ._abc import SequenceABC, TimeABC, VideoABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/bewegung/core/task.py b/src/bewegung/animation/_task.py similarity index 93% rename from src/bewegung/core/task.py rename to src/bewegung/animation/_task.py index 4703f7e..689c336 100644 --- a/src/bewegung/core/task.py +++ b/src/bewegung/animation/_task.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/task.py: Render task + src/bewegung/animation/_task.py: Render task Copyright (C) 2020-2021 Sebastian M. Ernst @@ -30,8 +30,8 @@ from typing import Any, Callable -from .abc import SequenceABC, TaskABC, TimeABC -from .typeguard import typechecked +from ..lib import typechecked +from ._abc import SequenceABC, TaskABC, TimeABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/bewegung/core/time.py b/src/bewegung/animation/_time.py similarity index 97% rename from src/bewegung/core/time.py rename to src/bewegung/animation/_time.py index 27b9b2d..1b15859 100644 --- a/src/bewegung/core/time.py +++ b/src/bewegung/animation/_time.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/time.py: Time handling + src/bewegung/animation/_time.py: Time handling Copyright (C) 2020-2021 Sebastian M. Ernst @@ -30,9 +30,9 @@ from typing import Generator, Union -from .abc import TimeABC -from .const import FPS_DEFAULT -from .typeguard import typechecked +from ..lib import typechecked +from ._abc import TimeABC +from ._const import FPS_DEFAULT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/bewegung/core/timescale.py b/src/bewegung/animation/_timescale.py similarity index 91% rename from src/bewegung/core/timescale.py rename to src/bewegung/animation/_timescale.py index b677b51..c598922 100644 --- a/src/bewegung/core/timescale.py +++ b/src/bewegung/animation/_timescale.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/timescale.py: Time scaling + src/bewegung/animation/_timescale.py: Time scaling Copyright (C) 2020-2021 Sebastian M. Ernst @@ -29,11 +29,12 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ from datetime import datetime +from numbers import Number from typing import Type -from .abc import PyNumber, TimeABC, TimeScaleABC -from .time import Time -from .typeguard import typechecked +from ..lib import typechecked +from ._abc import TimeABC, TimeScaleABC +from ._time import Time # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS: TimeScale @@ -53,7 +54,7 @@ class TimeScale(TimeScaleABC): stop_scaled : A number representing the end of the the scaled time interval. Must have the same datatype as ``start_scaled``. """ - def __init__(self, start: TimeABC, start_scaled: PyNumber, stop: TimeABC, stop_scaled: PyNumber): + def __init__(self, start: TimeABC, start_scaled: Number, stop: TimeABC, stop_scaled: Number): if start.fps != stop.fps: raise ValueError() @@ -117,7 +118,7 @@ def stop(self) -> TimeABC: return self._stop @property - def start_scaled(self) -> PyNumber: + def start_scaled(self) -> Number: """ Scaled start time """ @@ -125,14 +126,14 @@ def start_scaled(self) -> PyNumber: return self._start_scaled @property - def stop_scaled(self) -> PyNumber: + def stop_scaled(self) -> Number: """ Scaled stop time """ return self._stop_scaled - def scaled2time(self, scaled_time: PyNumber) -> TimeABC: + def scaled2time(self, scaled_time: Number) -> TimeABC: """ Converts scaled time to time in animation @@ -148,7 +149,7 @@ def scaled2time(self, scaled_time: PyNumber) -> TimeABC: return Time(fps = self._start.fps, index = self._start.index + index) - def time2scaled(self, time: TimeABC) -> PyNumber: + def time2scaled(self, time: TimeABC) -> Number: """ Converts time in animation to scaled time diff --git a/src/bewegung/core/video.py b/src/bewegung/animation/_video.py similarity index 97% rename from src/bewegung/core/video.py rename to src/bewegung/animation/_video.py index d14ef1d..a42f466 100644 --- a/src/bewegung/core/video.py +++ b/src/bewegung/animation/_video.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/video.py: Parallel video frame renderer + src/bewegung/animation/_video.py: Parallel video frame renderer Copyright (C) 2020-2021 Sebastian M. Ernst @@ -38,17 +38,17 @@ except ModuleNotFoundError: tqdm = lambda x: x -from .abc import EncoderABC, LayerABC, SequenceABC, VideoABC, Vector2DABC, TimeABC -from .backends import backends -from .const import FPS_DEFAULT -from .encoders import FFmpegH264Encoder -from .indexpool import IndexPool -from .layer import Layer -from .sequence import Sequence -from .task import Task -from .time import Time -from .typeguard import typechecked -from .vector import Vector2D +from ..lib import typechecked +from ..linalg import Vector2D +from ._abc import EncoderABC, LayerABC, SequenceABC, VideoABC, TimeABC +from ._backends import backends +from ._const import FPS_DEFAULT +from ._encoders import FFmpegH264Encoder +from ._indexpool import IndexPool +from ._layer import Layer +from ._sequence import Sequence +from ._task import Task +from ._time import Time # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # "GLOBALS" (FOR WORKERS) @@ -375,7 +375,7 @@ def wrapper(sequence: SequenceABC, time: Time): def layer(self, zindex: Union[int, None] = None, canvas: Union[Callable, None] = None, - offset: Union[Vector2DABC, None] = None, + offset: Union[Vector2D, None] = None, ) -> Callable: """ A **decorator** for decorating ``layer`` methods (tasks) within ``sequence`` classes. diff --git a/src/bewegung/contrib/circular_century_calendar.py b/src/bewegung/contrib/circular_century_calendar.py index 85bfb67..cdb33e9 100644 --- a/src/bewegung/contrib/circular_century_calendar.py +++ b/src/bewegung/contrib/circular_century_calendar.py @@ -35,12 +35,9 @@ from PIL.Image import Image, new, LANCZOS -from ..core.canvas import inventory -from ..core.color import Color -from ..core.vector import Matrix, Vector2D -from ..core.typeguard import typechecked - -DrawingBoard = inventory['drawingboard'].type +from ..lib import Color, typechecked +from ..drawingboard import DrawingBoard +from ..linalg import Matrix, Vector2D # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/bewegung/core/vector/array2ddist.py b/src/bewegung/core/vector/array2ddist.py deleted file mode 100644 index 86e2502..0000000 --- a/src/bewegung/core/vector/array2ddist.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - -BEWEGUNG -a versatile video renderer -https://github.com/pleiszenburg/bewegung - - src/bewegung/core/vector/array2ddist.py: 2D Vector Array with distance parameter - - Copyright (C) 2020-2021 Sebastian M. Ernst - - -The contents of this file are subject to the GNU Lesser General Public License -Version 2.1 ("LGPL" or "License"). You may not use this file except in -compliance with the License. You may obtain a copy of the License at -https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -https://github.com/pleiszenburg/bewegung/blob/master/LICENSE - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - - -""" - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# IMPORT -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -from typing import List, Union - -import numpy as np -from typeguard import typechecked - -from .lib import dtype_np2py -from .single2ddist import Vector2Ddist -from .array2d import VectorArray2D -from ..abc import Dtype, Number, Vector2DABC, VectorArray2DABC, VectorIterable2D -from ..const import FLOAT_DEFAULT - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# CLASS -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -@typechecked -class VectorArray2Ddist(VectorArray2D): - """ - Version of :class:`bewegung.VectorArray2D` with distance parameter - - Mutable. - - Args: - x : x components. Must have the same type like ``y`` and ``dist``. - y : y components. Must have the same type like ``x`` and ``dist``. - dist : Distance components. Must have the same type like ``x`` and ``y``. - """ - - def __init__(self, x: np.ndarray, y: np.ndarray, dist: np.ndarray): - assert dist.ndim == 1 - assert x.shape[0] == y.shape[0] == dist.shape[0] - assert x.dtype == y.dtype == dist.dtype - x.setflags(write = False) - y.setflags(write = False) - dist.setflags(write = False) - super().__init__(x = x, y = y,) - self._dist = dist - - def __repr__(self) -> str: - """ - String representation for interactive use - """ - - return f'' - - def __getitem__(self, idx: Union[int, slice]) -> Union[Vector2Ddist, VectorArray2DABC]: - """ - Item access, returning an independent object - either - a :class:`bewegung.Vector2Ddist` (index access) or - a :class:`bewegung.VectorArray2Ddist` (slicing) - - Args: - idx : Either an index or a slice - """ - - if isinstance(idx, int): - dtype = dtype_np2py(self.dtype) - return Vector2Ddist(dtype(self._x[idx]), dtype(self._y[idx]), dtype(self._dist[idx]), dtype = dtype) - - return VectorArray2Ddist(self._x[idx].copy(), self._y[idx].copy(), self._dist[idx].copy()) - - def mul(self, scalar: Number): - "" - raise NotImplementedError() - - def as_vectorarray(self) -> VectorArray2DABC: - """ - Exports a vector array without distance component - """ - - return VectorArray2D(self._x.copy(), self._y.copy()) - - def as_list(self) -> List[Vector2DABC]: - """ - Exports a list of :class:`bewegung.Vector2Ddist` objects - """ - - dtype = dtype_np2py(self.dtype) - return [ - Vector2Ddist(dtype(self._x[idx]), dtype(self._y[idx]), dtype(self._dist[idx]), dtype = dtype) - for idx in range(len(self)) - ] - - def as_ndarray(self, dtype: Dtype = FLOAT_DEFAULT) -> np.ndarray: - """ - Exports vector distance array as a ``numpy.ndarry`` object, shape ``(len(self), 3)`` (x, y, dist). - - Args: - dtype : Desired ``numpy`` data type of new vector - """ - - a = np.zeros((len(self), 3), dtype = self.dtype) - a[:, 0], a[:, 1], a[:, 2] = self._x, self._y, self._dist - return a if a.dtype == np.dtype(dtype) else a.astype(dtype) - - def as_type(self, dtype: Dtype) -> VectorArray2DABC: - """ - Exports vector distance array as another vector distance array with new dtype - - Args: - dtype : Desired ``numpy`` data type of new vector distance array - """ - - return self.copy() if self.dtype == np.dtype(dtype) else VectorArray2Ddist( - self._x.astype(dtype), self._y.astype(dtype), self._dist.astype(dtype), - ) - - def copy(self) -> VectorArray2DABC: - """ - Copies vector distance array - """ - - return VectorArray2Ddist(self._x.copy(), self._y.copy(), self._dist.copy()) - - def update_from_vector(self, other: VectorArray2DABC): - "" - raise NotImplementedError() - - @property - def dist(self) -> np.ndarray: - """ - Distance components, mutable - """ - - return self._dist - - @dist.setter - def dist(self, value: float): - "" - raise NotImplementedError() - - @classmethod - def from_iterable(cls, obj: VectorIterable2D, dtype: Dtype = FLOAT_DEFAULT) -> VectorArray2DABC: - """ - Generates vector distance array object from an iterable of :class:`bewegung.Vector2Ddist` objects - - Args: - obj : iterable - dtype : Desired ``numpy`` data type of new vector array - """ - - if not isinstance(obj, list): - obj = list(obj) - assert all((isinstance(item, Vector2Ddist) for item in obj)) - x = np.zeros((len(obj),), dtype = dtype) - y = np.zeros((len(obj),), dtype = dtype) - dist = np.zeros((len(obj),), dtype = dtype) - for idx, item in enumerate(obj): - x[idx], y[idx], dist[idx] = item.x, item.y, item.dist - return cls(x = x, y = y, dist = dist,) - - @classmethod - def from_polar(cls, radius: np.ndarray, angle: np.ndarray) -> VectorArray2DABC: - "" - raise NotImplementedError() diff --git a/src/bewegung/core/vector/matrix.py b/src/bewegung/core/vector/matrix.py deleted file mode 100644 index 926bbc3..0000000 --- a/src/bewegung/core/vector/matrix.py +++ /dev/null @@ -1,220 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - -BEWEGUNG -a versatile video renderer -https://github.com/pleiszenburg/bewegung - - src/bewegung/core/vector/matrix.py: Simple 2x2/3x3 matrix for rotations - - Copyright (C) 2020-2021 Sebastian M. Ernst - - -The contents of this file are subject to the GNU Lesser General Public License -Version 2.1 ("LGPL" or "License"). You may not use this file except in -compliance with the License. You may obtain a copy of the License at -https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -https://github.com/pleiszenburg/bewegung/blob/master/LICENSE - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - - -""" - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# IMPORT -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -from math import cos, sin -from typing import List, Tuple, Type, Union - -try: - import numpy as np - from numpy import ndarray -except ModuleNotFoundError: - np, ndarray = None, None -from typeguard import typechecked - -from ..abc import ( - Dtype, MatrixABC, PyNumber, - Vector2DABC, Vector3DABC, - VectorArray2DABC, VectorArray3DABC, - ) -from ..const import FLOAT_DEFAULT -from .single2d import Vector2D -from .single3d import Vector3D -from .array2d import VectorArray2D -from .array3d import VectorArray3D - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# CLASS -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -@typechecked -class Matrix(MatrixABC): - """ - A simple matrix implementation for rotating vectors - - Mutable. - - Args: - matrix : 2D or 3D arrangement in a list of lists containing Python numbers - dtype : Data type. Derived from entries in ``matrix`` if not explicitly provided. - """ - - def __init__(self, matrix = List[List[PyNumber]], dtype: Union[Type, None] = None): - - lines = len(matrix) - assert lines in (2, 3) # allow 2D and 3D - assert all((len(line) == lines for line in matrix)) - - if dtype is None: - dtype = type(matrix[0][0]) - else: - assert isinstance(matrix[0][0], dtype) - assert all((all((isinstance(number, dtype) for number in line)) for line in matrix)) - - self._dtype = dtype - self._matrix = matrix - - def __repr__(self) -> str: - """ - String representation for interactive use - """ - - return f'' - - def __matmul__( - self, - vector: Union[Vector2DABC, Vector3DABC, VectorArray2DABC, VectorArray3DABC] - ) -> Union[Vector2DABC, Vector3DABC, VectorArray2DABC, VectorArray3DABC]: - """ - Multiplies the matrix with a vector or array of vectors - and returns the resulting new vector or array of vectors. - Raises an exception if matrix and vector or - array of vectors have different numbers of dimensions. - - Args: - vector : A 2D or 3D vector or array of vectors - """ - - vector_tuple = vector.as_tuple() - assert self.ndim == len(vector_tuple) - - values = [ - sum([trigonometric * dimension for trigonometric, dimension in zip(line, vector_tuple)]) - for line in self._matrix - ] - - if any((isinstance(vector, datatype) for datatype in (Vector2DABC, Vector3DABC))): - return Vector2D(*values) if len(vector_tuple) == 2 else Vector3D(*values) - return VectorArray2D(*values) if len(vector_tuple) == 2 else VectorArray3D(*values) - - def __getitem__(self, index: Tuple[int, int]) -> PyNumber: - """ - Item access, returns value at position - - Args: - index : Row and column index - """ - - return self._matrix[index[0]][index[1]] - - def __setitem__(self, index: Tuple[int, int], value: PyNumber): - """ - Item access, sets new value at position - - Args: - index : Row and column index - value : New value - """ - - self._matrix[index[0]][index[1]] = self._dtype(value) - - def as_ndarray(self, dtype: Dtype = FLOAT_DEFAULT) -> ndarray: - """ - Exports matrix as a ``numpy.ndarry`` object, shape ``(2, 2)`` or ``(3, 3)``. - - Args: - dtype : Desired ``numpy`` data type of new vector - """ - - if np is None: - raise NotImplementedError('numpy is not available') - - return np.array(self._matrix, dtype = dtype) - - @property - def dtype(self) -> Type: - """ - (Python) data type of matrix components - """ - - return self._dtype - - @property - def ndim(self) -> int: - """ - Number of dimensions, either ``2`` or ``3``. - """ - - return len(self._matrix) - - @classmethod - def from_ndarray(cls, matrix: ndarray, dtype: Type = float) -> MatrixABC: - """ - Generates new matrix object from ``numpy.ndarray`` object - of shape ``(2, 2)`` or ``(3, 3)`` - - Args: - matrix : Input data - dtype : Desired (Python) data type of matrix - """ - - assert matrix.ndim == 2 - assert matrix.shape in ((2, 2), (3, 3)) - - matrix = matrix.tolist() - if isinstance(matrix[0][0], int): - matrix = [[dtype(item) for item in line] for line in matrix] - - return cls(matrix, dtype = dtype) - - @classmethod - def from_2d_rotation(cls, a: PyNumber) -> MatrixABC: - """ - Generates new 2D matrix object from an angle - - Args: - a : An angle in radians - """ - - sa, ca = sin(a), cos(a) - - return cls([ - [ca, -sa], - [sa, ca], - ]) - - @classmethod - def from_3d_rotation(cls, v: Vector3DABC, a: PyNumber) -> MatrixABC: - """ - Generates new 3D matrix object from a vector and an angle - - Args: - v : A 3D vector - a : An angle in radians - """ - - ca = cos(a) - oca = 1 - ca - sa = sin(a) - - return cls([ - [ca + (v.x ** 2) * oca, v.x * v.y * oca - v.z * sa, v.x * v.y * oca + v.y * sa], - [v.y * v.x * oca + v.z * sa, ca + (v.y ** 2) * oca, v.y * v.z * oca - v.x * sa], - [v.z * v.x * oca - v.y * sa, v.z * v.y * oca + v.x * sa, ca + (v.z ** 2) * oca], - ]) diff --git a/src/bewegung/core/vector/matrixarray.py b/src/bewegung/core/vector/matrixarray.py deleted file mode 100644 index 6a8bbe9..0000000 --- a/src/bewegung/core/vector/matrixarray.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - -BEWEGUNG -a versatile video renderer -https://github.com/pleiszenburg/bewegung - - src/bewegung/core/vector/matrixarray.py: Array of simple 2x2/3x3 matrices for rotations - - Copyright (C) 2020-2021 Sebastian M. Ernst - - -The contents of this file are subject to the GNU Lesser General Public License -Version 2.1 ("LGPL" or "License"). You may not use this file except in -compliance with the License. You may obtain a copy of the License at -https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -https://github.com/pleiszenburg/bewegung/blob/master/LICENSE - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - - -""" - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# IMPORT -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -from typing import List, Tuple, Type, Union - -import numpy as np -from numpy import ndarray - -from typeguard import typechecked - -from ..abc import ( - Dtype, MatrixArrayABC, PyNumber, - Vector2DABC, Vector3DABC, - VectorArray2DABC, VectorArray3DABC, - ) -from ..const import FLOAT_DEFAULT -from .lib import dtype_np2py -from .single2d import Vector2D -from .single3d import Vector3D -from .array2d import VectorArray2D -from .array3d import VectorArray3D -from .matrix import Matrix - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# CLASS -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -@typechecked -class MatrixArray(MatrixArrayABC): - """ - An array implementation of simple matrices for rotating vector arrays - - Mutable. - - TODO Stub! - - Args: - matrix : 2D or 3D arrangement in a list of lists containing Python numbers - dtype : Data type. Derived from entries in ``matrix`` if not explicitly provided. - """ - - # def __init__(self, matrix = List[List[ndarray]]): - # - # lines = len(matrix) - # assert lines in (2, 3) # allow 2D and 3D - # assert all((len(line) == lines for line in matrix)) - # - # self._length = matrix[0][0].shape[0] - # assert all(( - # (item.ndim == 1 and item.shape[0] == self._length) - # for line in matrix for item in line - # )) - # - # self._dtype = matrix[0][0].dtype - # assert all((item.dtype == self._dtype for line in matrix for item in line)) - # - # self._matrix = matrix - # - # def __repr__(self) -> str: - # """ - # String representation for interactive use - # """ - # - # return f'' - # - # def __len__(self) -> int: - # """ - # Length of array - # """ - # - # return self._length - # - # def __matmul__( - # self, - # vector: Union[Vector2DABC, Vector3DABC, VectorArray2DABC, VectorArray3DABC] - # ) -> Union[Vector2DABC, Vector3DABC, VectorArray2DABC, VectorArray3DABC]: - # """ - # Multiplies the matrix with a vector or array of vectors - # and returns the resulting new vector or array of vectors. - # Raises an exception if matrix and vector or - # array of vectors have different numbers of dimensions. - # - # Args: - # vector : A 2D or 3D vector or array of vectors - # """ - # - # vector_tuple = vector.as_tuple() - # assert self.ndim == len(vector_tuple) - # - # values = [ - # sum([trigonometric * dimension for trigonometric, dimension in zip(line, vector_tuple)]) - # for line in self._matrix - # ] - # - # if any((isinstance(vector, datatype) for datatype in (Vector2DABC, Vector3DABC))): - # return Vector2D(*values) if len(vector_tuple) == 2 else Vector3D(*values) - # return VectorArray2D(*values) if len(vector_tuple) == 2 else VectorArray3D(*values) - # - # def __getitem__(self, index: Union[Tuple[int, int, int], int, slice]) -> Union[PyNumber, Matrix]: - # """ - # Item access, returns value at position - # - # Args: - # index : Row, column and position index - # """ - # - # if isinstance(index, slice): - # return MatrixArray([ - # [self._matrix[0][0][index].copy(), self._matrix[0][1][index].copy()], - # [self._matrix[1][0][index].copy(), self._matrix[1][1][index].copy()], - # ]) - # - # dtype = dtype_np2py(self.dtype) - # - # if isinstance(index, int): - # return Matrix([ - # [dtype(self._matrix[0][0][index]), dtype(self._matrix[0][1][index])], - # [dtype(self._matrix[1][0][index]), dtype(self._matrix[1][1][index])], - # ]) - # - # return dtype(self._matrix[index[0]][index[1]][index[2]]) - # - # def __setitem__(self, index: Tuple[int, int], value: PyNumber): - # """ - # Item access, sets new value at position - # - # Args: - # index : Row and column index - # value : New value - # """ - # - # self._matrix[index[0]][index[1]] = self._dtype(value) - # - # def as_ndarray(self, dtype: Dtype = FLOAT_DEFAULT) -> ndarray: - # """ - # Exports matrix as a ``numpy.ndarry`` object, shape ``(2, 2)`` or ``(3, 3)``. - # - # Args: - # dtype : Desired ``numpy`` data type of new vector - # """ - # - # return np.array(self._matrix, dtype = dtype) - # - # @property - # def dtype(self) -> Type: - # """ - # (Python) data type of matrix components - # """ - # - # return self._dtype - # - # @property - # def ndim(self) -> int: - # """ - # Number of dimensions, either ``2`` or ``3``. - # """ - # - # return len(self._matrix) - # - # @classmethod - # def from_ndarray(cls, matrix: ndarray, dtype: Type = float) -> MatrixArrayABC: - # """ - # Generates new matrix object from ``numpy.ndarray`` object - # of shape ``(2, 2)`` or ``(3, 3)`` - # - # Args: - # matrix : Input data - # dtype : Desired (Python) data type of matrix - # """ - # - # assert matrix.ndim == 2 - # assert matrix.shape in ((2, 2), (3, 3)) - # - # matrix = matrix.tolist() - # if isinstance(matrix[0][0], int): - # matrix = [[dtype(item) for item in line] for line in matrix] - # - # return cls(matrix) - # - # @classmethod - # def from_2d_rotation(cls, a: ndarray) -> MatrixArrayABC: - # """ - # Generates new 2D matrix object from an angle - # - # Args: - # a : An array of angles in radians - # """ - # - # assert a.ndim == 1 - # sa, ca = np.sin(a), np.cos(a) - # - # return cls([ - # [ca, -sa], - # [sa, ca], - # ]) - # - # @classmethod - # def from_3d_rotation(cls, v: Vector3DABC, a: PyNumber) -> MatrixArrayABC: - # """ - # Generates new 3D matrix object from a vector and an angle - # - # Args: - # v : A 3D vector - # a : An angle in radians - # """ - # - # ca = cos(a) - # oca = 1 - ca - # sa = sin(a) - # - # return cls([ - # [ca + (v.x ** 2) * oca, v.x * v.y * oca - v.z * sa, v.x * v.y * oca + v.y * sa], - # [v.y * v.x * oca + v.z * sa, ca + (v.y ** 2) * oca, v.y * v.z * oca - v.x * sa], - # [v.z * v.x * oca - v.y * sa, v.z * v.y * oca + v.x * sa, ca + (v.z ** 2) * oca], - # ]) diff --git a/src/bewegung/core/vector/single2ddist.py b/src/bewegung/core/vector/single2ddist.py deleted file mode 100644 index 4b75df1..0000000 --- a/src/bewegung/core/vector/single2ddist.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - -BEWEGUNG -a versatile video renderer -https://github.com/pleiszenburg/bewegung - - src/bewegung/core/vector/single2ddist.py: Single 2D Vector with distance parameter - - Copyright (C) 2020-2021 Sebastian M. Ernst - - -The contents of this file are subject to the GNU Lesser General Public License -Version 2.1 ("LGPL" or "License"). You may not use this file except in -compliance with the License. You may obtain a copy of the License at -https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -https://github.com/pleiszenburg/bewegung/blob/master/LICENSE - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - - -""" - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# IMPORT -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -from typeguard import typechecked - -from typing import Type, Union - -from .single2d import Vector2D -from ..abc import PyNumber, Vector2DABC - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# CLASS -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -@typechecked -class Vector2Ddist(Vector2D): - """ - Immutable version of :class:`bewegung.Vector2D` with distance parameter - - Args: - x : x component. Must have the same type like ``y`` and ``dist``. - y : y component. Must have the same type like ``x`` and ``dist``. - dist : Distance component. Must have the same type like ``x`` and ``y``. - dtype : Data type. Derived from ``x`` and ``y`` if not explicitly provided. - """ - - def __init__(self, x: PyNumber, y: PyNumber, dist: PyNumber, dtype: Union[Type, None] = None): - - super().__init__(x = x, y = y, dtype = dtype) - assert isinstance(dist, self._dtype) - self._dist = dist - - def __repr__(self) -> str: - """ - String representation for interactive use - """ - - if self._dtype == int: - return f'' - return f'' - - def mul(self, scalar: PyNumber): - "" - raise NotImplementedError() - - def as_vector(self) -> Vector2DABC: - """ - Exports a vector without distance component - """ - - return Vector2D(self._x, self._y, dtype = self._dtype) - - def copy(self) -> Vector2DABC: - """ - Copies vector with distance component - """ - - return type(self)(self._x, self._y, self._dist, dtype = self._dtype) - - def update(self, x: PyNumber, y: PyNumber): - "" - raise NotImplementedError() - - def update_from_vector(self, other: Vector2DABC): - "" - raise NotImplementedError() - - @property - def x(self) -> PyNumber: - """ - x component - """ - - return self._x - - @x.setter - def x(self, value: PyNumber): - "" - raise NotImplementedError() - - @property - def y(self) -> PyNumber: - """ - y component - """ - - return self._y - - @y.setter - def y(self, value: PyNumber): - "" - raise NotImplementedError() - - @property - def dist(self) -> PyNumber: - """ - Distance component - """ - - return self._dist - - @dist.setter - def dist(self, value: PyNumber): - "" - raise NotImplementedError() - - @classmethod - def from_polar(cls, radius: PyNumber, angle: PyNumber) -> Vector2DABC: - "" - raise NotImplementedError() diff --git a/src/bewegung/core/backends/drawingboard/__init__.py b/src/bewegung/drawingboard/__init__.py similarity index 90% rename from src/bewegung/core/backends/drawingboard/__init__.py rename to src/bewegung/drawingboard/__init__.py index bf35a89..2c142e6 100644 --- a/src/bewegung/core/backends/drawingboard/__init__.py +++ b/src/bewegung/drawingboard/__init__.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/drawingboard/__init__.py: Simple 2D cairo renderer + src/bewegung/drawingboard/__init__.py: Simple 2D cairo renderer Copyright (C) 2020-2021 Sebastian M. Ernst @@ -28,4 +28,4 @@ # EXPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from .backend import Backend +from ._core import DrawingBoard diff --git a/src/bewegung/drawingboard/_abc.py b/src/bewegung/drawingboard/_abc.py new file mode 100644 index 0000000..eb9d968 --- /dev/null +++ b/src/bewegung/drawingboard/_abc.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/drawingboard/_abc.py: Abstract base classes + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from abc import ABC + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASSES +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class DrawingBoardABC(ABC): + pass diff --git a/src/bewegung/core/backends/drawingboard/core.py b/src/bewegung/drawingboard/_core.py similarity index 97% rename from src/bewegung/core/backends/drawingboard/core.py rename to src/bewegung/drawingboard/_core.py index 7aa50bd..6eebc35 100644 --- a/src/bewegung/core/backends/drawingboard/core.py +++ b/src/bewegung/drawingboard/_core.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/backends/drawingboard/core.py: Simple 2D cairo renderer + src/bewegung/drawingboard/_core.py: Simple 2D cairo renderer Copyright (C) 2020-2021 Sebastian M. Ernst @@ -62,10 +62,9 @@ class Handle: except ModuleNotFoundError: IPython = None -from ...abc import DrawingBoardABC, Vector2DABC -from ...color import Color -from ...typeguard import typechecked -from ...vector import Vector2D, Matrix +from ..lib import Color, typechecked +from ..linalg import Vector2D, Matrix +from ._abc import DrawingBoardABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -204,7 +203,7 @@ def draw_svg(self, fn: Union[str, None] = None, raw: Union[bytes, None] = None, svg: Union[Rsvg.Handle, None] = None, - point: Union[Vector2DABC, None] = None, + point: Union[Vector2D, None] = None, scale: float = 1.0, angle: float = 0.0, anchor: Union[Vector2D, str] = 'cc', @@ -302,7 +301,7 @@ def make_svg( @_geometry def draw_text(self, text: str = '', - point: Union[Vector2DABC, None] = None, + point: Union[Vector2D, None] = None, angle: float = 0.0, font: Union[Pango.FontDescription, None] = None, font_color: Union[Color, None] = None, @@ -393,7 +392,7 @@ def make_font(family: str, size: float) -> Pango.FontDescription: @_geometry def draw_polygon(self, - *points: Vector2DABC, + *points: Vector2D, close: bool = False, **kwargs, ): @@ -418,7 +417,7 @@ def draw_polygon(self, @_geometry def draw_filledpolygon(self, - *points: Vector2DABC, + *points: Vector2D, fill_color: Union[Color, None] = None, ): """ @@ -442,10 +441,10 @@ def draw_filledpolygon(self, @_geometry def draw_bezier(self, - a: Vector2DABC, - b: Vector2DABC, - c: Vector2DABC, - d: Vector2DABC, + a: Vector2D, + b: Vector2D, + c: Vector2D, + d: Vector2D, **kwargs, ): """ @@ -465,7 +464,7 @@ def draw_bezier(self, @_geometry def draw_circle(self, - point: Vector2DABC, + point: Vector2D, r: float = 1.0, **kwargs, ): @@ -489,7 +488,7 @@ def draw_circle(self, @_geometry def draw_filledcircle(self, - point: Vector2DABC, + point: Vector2D, r: float = 1.0, fill_color: Union[Color, None] = None, ): diff --git a/src/bewegung/lib/__init__.py b/src/bewegung/lib/__init__.py new file mode 100644 index 0000000..a84ea5f --- /dev/null +++ b/src/bewegung/lib/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/lib/__init__.py: Common routines and classes + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# EXPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from ._color import Color +from ._typeguard import typechecked diff --git a/src/bewegung/lib/_abc.py b/src/bewegung/lib/_abc.py new file mode 100644 index 0000000..60906e8 --- /dev/null +++ b/src/bewegung/lib/_abc.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/lib/_abc.py: Abstract base classes + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from abc import ABC + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASSES +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class ColorABC(ABC): + pass diff --git a/src/bewegung/core/color.py b/src/bewegung/lib/_color.py similarity index 80% rename from src/bewegung/core/color.py rename to src/bewegung/lib/_color.py index 0e15757..46889d1 100644 --- a/src/bewegung/core/color.py +++ b/src/bewegung/lib/_color.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/color.py: Simple RGBA Color handling + src/bewegung/lib/_color.py: Simple RGBA Color handling Copyright (C) 2020-2021 Sebastian M. Ernst @@ -28,10 +28,11 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +from math import floor from typing import Tuple -from .abc import ColorABC -from .typeguard import typechecked +from ._abc import ColorABC +from ._typeguard import typechecked # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -201,3 +202,49 @@ def from_hex(cls, raw: str) -> ColorABC: ) if not (color == 'a' and len(component) == 0) }) + + @classmethod + def from_hsv(cls, + h: float, + s: float, + v: float, + ): + """ + Imports color from HSV + + Args: + h : hue 0.0...360.0 + s : saturation 0.0...1.0 + v : value (brightness) 0.0...1.0 + """ + + if not (0.0 <= h <= 360.0): + raise ValueError('hue value out of bounds (0.0...360.0)') + if not (0.0 <= s <= 1.0): + raise ValueError('saturation value out of bounds (0.0...1.0)') + if not (0.0 <= v <= 1.0): + raise ValueError('"value" (brightness) value out of bounds (0.0...1.0)') + + hi = floor(h / 60) + + f = h / 60 - hi + + p = round(255 * v * (1 - s)) + q = round(255 * v * (1 - s * f)) + t = round(255 * v * (1 - s * (1 - f))) + v = round(255 * v) + + if hi == 1: + return cls(q, v, p) + if hi == 2: + return cls(p, v, t) + if hi == 3: + return cls(p, q, v) + if hi == 4: + return cls(t, p, v) + if hi == 5: + return cls(v, p, q) + if hi in (0, 6): + return cls(v, t, p) + + raise ValueError('Conversion from HSV to RGB failed') diff --git a/src/bewegung/core/typeguard.py b/src/bewegung/lib/_typeguard.py similarity index 94% rename from src/bewegung/core/typeguard.py rename to src/bewegung/lib/_typeguard.py index 0406123..009e9e2 100644 --- a/src/bewegung/core/typeguard.py +++ b/src/bewegung/lib/_typeguard.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/typeguard.py: Wrapper around typeguard (optional) + src/bewegung/lib/_typeguard.py: Wrapper around typeguard (optional) Copyright (C) 2020-2021 Sebastian M. Ernst diff --git a/src/bewegung/core/vector/__init__.py b/src/bewegung/linalg/__init__.py similarity index 70% rename from src/bewegung/core/vector/__init__.py rename to src/bewegung/linalg/__init__.py index ae7b325..5502dc9 100644 --- a/src/bewegung/core/vector/__init__.py +++ b/src/bewegung/linalg/__init__.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/vector/__init__.py: Vector algebra module root + src/bewegung/linalg/__init__.py: Linear algebra module root Copyright (C) 2020-2021 Sebastian M. Ernst @@ -28,20 +28,22 @@ # EXPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from .single2d import Vector2D -from .single2ddist import Vector2Ddist -from .single3d import Vector3D -from .matrix import Matrix +from ._const import FLOAT_DEFAULT -try: - import numpy as _np -except ModuleNotFoundError: - _np = None +from ._single import Vector +from ._array import VectorArray + +from ._single2d import Vector2D +from ._single3d import Vector3D +from ._matrix import Matrix + +from ._numpy import np as _np if _np is not None: - from .array2d import VectorArray2D - from .array2ddist import VectorArray2Ddist - from .array3d import VectorArray3D - from .matrixarray import MatrixArray + from ._array2d import VectorArray2D + from ._array3d import VectorArray3D + from ._matrixarray import MatrixArray del _np + +from ._camera import Camera diff --git a/src/bewegung/core/abc.py b/src/bewegung/linalg/_abc.py similarity index 61% rename from src/bewegung/core/abc.py rename to src/bewegung/linalg/_abc.py index 6e7b60b..9487fb5 100644 --- a/src/bewegung/core/abc.py +++ b/src/bewegung/linalg/_abc.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/abc.py: Abstract base classes + src/bewegung/linalg/_abc.py: Abstract base classes Copyright (C) 2020-2021 Sebastian M. Ernst @@ -29,59 +29,24 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ from abc import ABC -from typing import Generator, List, Tuple, Union +from numbers import Number +from typing import Dict, Tuple, Type, TypeVar, Union -try: - import numpy as np -except ModuleNotFoundError: - np = None +from ._numpy import np, ndarray # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASSES # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -class BackendABC(ABC): - pass - class CameraABC(ABC): pass -class ColorABC(ABC): - pass - -class DrawingBoardABC(ABC): - pass - -class EffectABC(ABC): - pass - -class EncoderABC(ABC): - pass - -class IndexPoolABC(ABC): - pass - class MatrixABC(ABC): pass class MatrixArrayABC(ABC): pass -class LayerABC(ABC): - pass - -class SequenceABC(ABC): - pass - -class TaskABC(ABC): - pass - -class TimeABC(ABC): - pass - -class TimeScaleABC(ABC): - pass - class VectorArray2DABC(ABC): pass @@ -94,34 +59,25 @@ class Vector2DABC(ABC): class Vector3DABC(ABC): pass -class VideoABC(ABC): - pass - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Types # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -PyNumber = Union[int, float] -PyNumber2D = Union[Tuple[int, int], Tuple[float, float]] -PyNumber3D = Union[Tuple[int, int, int], Tuple[float, float, float]] +NumberType = Type[Number] if np is not None: - Number = Union[int, float, np.number] - NumberTypes = (int, float, np.number) - Dtype = Union[str, np.dtype] + Dtype = Union[str, NumberType, np.dtype] else: - Number = Union[int, float] - NumberTypes = (int, float) Dtype = None # HACK -VectorIterable2D = Union[ - List[Vector2DABC], - Tuple[Vector2DABC], - Generator[Vector2DABC, None, None], -] - -VectorIterable3D = Union[ - List[Vector3DABC], - Tuple[Vector3DABC], - Generator[Vector3DABC, None, None], -] +Numbers = TypeVar('N', bound = Number) +Number2D = Tuple[Numbers, Numbers] +Number3D = Tuple[Numbers, Numbers, Numbers] + +try: + from typing import NotImplementedType # re-introduced in Python 3.10 +except ImportError: + NotImplementedType = type(NotImplemented) + +MetaDict = Dict[str, Union[str, bytes, Number]] +MetaArrayDict = Dict[str, ndarray] diff --git a/src/bewegung/linalg/_array.py b/src/bewegung/linalg/_array.py new file mode 100644 index 0000000..3be3199 --- /dev/null +++ b/src/bewegung/linalg/_array.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/linalg/_array.py: Array base class + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from abc import ABC, abstractmethod +from collections.abc import Iterable +from typing import Union + +from ..lib import typechecked +from ._abc import MetaArrayDict + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class VectorArray(ABC, Iterable): + """ + Abstract base class for all vector array types. + + Not intended to be instantiated. + + Args: + meta : A dict holding arbitrary metadata + """ + + @abstractmethod + def __init__(self, meta: Union[MetaArrayDict, None] = None): + + meta = {} if meta is None else dict(meta) + + if not all(value.ndim == 1 for value in meta.values()): + raise ValueError('inconsistent: meta_value.ndim != 1') + if not all(value.shape[0] == len(self) for value in meta.values()): + raise ValueError('inconsistent length') + + self._meta = meta + + @property + def meta(self) -> MetaArrayDict: + """ + meta data dict + """ + + return self._meta diff --git a/src/bewegung/core/vector/array2d.py b/src/bewegung/linalg/_array2d.py similarity index 57% rename from src/bewegung/core/vector/array2d.py rename to src/bewegung/linalg/_array2d.py index 821a0da..6283be7 100644 --- a/src/bewegung/core/vector/array2d.py +++ b/src/bewegung/linalg/_array2d.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/vector/array2d.py: 2D Vector Array + src/bewegung/linalg/_array2d.py: 2D Vector Array Copyright (C) 2020-2021 Sebastian M. Ernst @@ -28,22 +28,29 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from typing import List, Tuple, Union - -import numpy as np -from typeguard import typechecked - -from .lib import dtype_np2py -from .single2d import Vector2D -from ..abc import Dtype, Number, VectorArray2DABC, VectorIterable2D -from ..const import FLOAT_DEFAULT +from collections.abc import Iterable +from numbers import Number +from typing import Any, List, Tuple, Union + +from ..lib import typechecked +from ._abc import ( + Dtype, + MetaArrayDict, + NotImplementedType, + VectorArray2DABC, +) +from ._array import VectorArray +from ._const import FLOAT_DEFAULT +from ._lib import dtype_np2py, dtype_name +from ._numpy import np +from ._single2d import Vector2D # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @typechecked -class VectorArray2D(VectorArray2DABC): +class VectorArray2D(VectorArray, VectorArray2DABC): """ An array of vectors in 2D space. @@ -52,22 +59,35 @@ class VectorArray2D(VectorArray2DABC): Args: x : x components. Must have the same dtype like ``y``. y : y components. Must have the same dtype like ``x``. + meta : A dict holding arbitrary metadata. """ - def __init__(self, x: np.ndarray, y: np.ndarray): + def __init__(self, x: np.ndarray, y: np.ndarray, dtype: Union[Dtype, None] = None, meta: Union[MetaArrayDict, None] = None): + + if x.ndim != 1: + raise ValueError('inconsistent: x.ndim != 1') + if y.ndim != 1: + raise ValueError('inconsistent: x.ndim != 1') + if x.shape[0] != y.shape[0]: + raise ValueError('inconsistent length') + + if dtype is None: + if x.dtype != y.dtype: + raise TypeError('can not guess dtype - inconsistent') + else: + x = x if x.dtype == np.dtype(dtype) else x.astype(dtype) + y = y if y.dtype == np.dtype(dtype) else y.astype(dtype) - assert x.ndim == 1 - assert y.ndim == 1 - assert x.shape[0] == y.shape[0] - assert x.dtype == y.dtype self._x, self._y = x, y + self._iterstate = 0 + super().__init__(meta = meta) def __repr__(self) -> str: """ String representation for interactive use """ - return f'' + return f'' def __len__(self) -> int: """ @@ -88,11 +108,41 @@ def __getitem__(self, idx: Union[int, slice]) -> Union[Vector2D, VectorArray2DAB if isinstance(idx, int): dtype = dtype_np2py(self.dtype) - return Vector2D(dtype(self._x[idx]), dtype(self._y[idx]), dtype = dtype) + return Vector2D( + x = dtype(self._x[idx]), + y = dtype(self._y[idx]), + dtype = dtype, + meta = {key: value[idx] for key, value in self._meta.items()}, + ) + + return VectorArray2D( + x = self._x[idx].copy(), + y = self._y[idx].copy(), + meta = {key: value[idx].copy() for key, value in self._meta.items()}, + ) - return VectorArray2D(self._x[idx].copy(), self._y[idx].copy()) + def __iter__(self) -> VectorArray2DABC: + """ + Iterator interface (1/2) + """ + + self._iterstate = 0 + return self - def __eq__(self, other: VectorArray2DABC) -> bool: + def __next__(self) -> Vector2D: + """ + Iterator interface (2/2) + """ + + if self._iterstate == len(self): + self._iterstate = 0 # reset + raise StopIteration() + + value = self[self._iterstate] + self._iterstate += 1 # increment + return value + + def __eq__(self, other: Any) -> Union[bool, NotImplementedType]: """ Equality check between vector arrays @@ -100,9 +150,12 @@ def __eq__(self, other: VectorArray2DABC) -> bool: other : Another vector array of equal length """ + if not isinstance(other, VectorArray2DABC): + return NotImplemented + return np.array_equal(self.x, other.x) and np.array_equal(self.y, other.y) - def __mod__(self, other: VectorArray2DABC) -> bool: + def __mod__(self, other: Any) -> Union[bool, NotImplementedType]: """ Is-close check between vector arrays @@ -110,9 +163,12 @@ def __mod__(self, other: VectorArray2DABC) -> bool: other : Another vector array of equal length """ + if not isinstance(other, VectorArray2DABC): + return NotImplemented + return np.allclose(self.x, other.x) and np.allclose(self.y, other.y) - def __add__(self, other: Union[VectorArray2DABC, Vector2D]) -> VectorArray2DABC: + def __add__(self, other: Any) -> Union[VectorArray2DABC, NotImplementedType]: """ Add operation between vector arrays or a vector array and a vector @@ -120,16 +176,22 @@ def __add__(self, other: Union[VectorArray2DABC, Vector2D]) -> VectorArray2DABC: other : Another vector array of equal length """ + if not any(isinstance(other, t) for t in (VectorArray2DABC, Vector2D)): + return NotImplemented + if isinstance(other, VectorArray2DABC): - assert len(self) == len(other) - assert self.dtype == other.dtype + if len(self) != len(other): + raise ValueError('inconsistent length') + if self.dtype != other.dtype: + raise TypeError('inconsistent dtype') + return VectorArray2D(self.x + other.x, self.y + other.y) def __radd__(self, *args, **kwargs): return self.__add__(*args, **kwargs) - def __sub__(self, other: Union[VectorArray2DABC, Vector2D]) -> VectorArray2DABC: + def __sub__(self, other: Any) -> Union[VectorArray2DABC, NotImplementedType]: """ Substract operator between vector arrays or a vector array and a vector @@ -137,16 +199,22 @@ def __sub__(self, other: Union[VectorArray2DABC, Vector2D]) -> VectorArray2DABC: other : Another vector array of equal length """ + if not any(isinstance(other, t) for t in (VectorArray2DABC, Vector2D)): + return NotImplemented + if isinstance(other, VectorArray2DABC): - assert len(self) == len(other) - assert self.dtype == other.dtype + if len(self) != len(other): + raise ValueError('inconsistent length') + if self.dtype != other.dtype: + raise TypeError('inconsistent dtype') + return VectorArray2D(self.x - other.x, self.y - other.y) def __rsub__(self, *args, **kwargs): return self.__sub__(*args, **kwargs) - def __mul__(self, other: Number) -> VectorArray2DABC: + def __mul__(self, other: Any) -> Union[VectorArray2DABC, NotImplementedType]: """ Multiplication with scalar @@ -154,8 +222,15 @@ def __mul__(self, other: Number) -> VectorArray2DABC: other : A number """ + if not isinstance(other, Number): + return NotImplemented + return VectorArray2D(self._x * other, self._y * other) + def __rmul__(self, *args, **kwargs): + + return self.__mul__(*args, **kwargs) + def mul(self, scalar: Number): """ In-place multiplication with scalar @@ -167,7 +242,7 @@ def mul(self, scalar: Number): np.multiply(self._x, scalar, out = self._x) np.multiply(self._y, scalar, out = self._y) - def __matmul__(self, other: VectorArray2DABC) -> np.ndarray: + def __matmul__(self, other: Any) -> Union[np.ndarray, NotImplementedType]: """ Scalar product between vector arrays @@ -175,8 +250,14 @@ def __matmul__(self, other: VectorArray2DABC) -> np.ndarray: other : Another vector array of equal length """ - assert len(self) == len(other) - assert self.dtype == other.dtype + if not isinstance(other, VectorArray2DABC): + return NotImplemented + + if len(self) != len(other): + raise ValueError('inconsistent length') + if self.dtype != other.dtype: + raise TypeError('inconsistent dtype') + return self.x * other.x + self.y * other.y def as_list(self) -> List[Vector2D]: @@ -184,11 +265,7 @@ def as_list(self) -> List[Vector2D]: Exports a list of :class:`bewegung.Vector2D` objects """ - dtype = dtype_np2py(self.dtype) - return [ - Vector2D(dtype(self._x[idx]), dtype(self._y[idx]), dtype = dtype) - for idx in range(len(self)) - ] + return list(self) def as_ndarray(self, dtype: Dtype = FLOAT_DEFAULT) -> np.ndarray: """ @@ -209,12 +286,18 @@ def as_polar_tuple(self) -> Tuple[np.ndarray, np.ndarray]: return self.mag, self.angle - def as_tuple(self) -> Tuple[np.ndarray, np.ndarray]: + def as_tuple(self, copy: bool = True) -> Tuple[np.ndarray, np.ndarray]: """ Exports vector array as a tuple of vector components in ``numpy.ndarry`` objects + + Args: + copy : Provide a copy of underlying ``numpy.ndarry`` """ - return self._x.copy(), self._y.copy() + if copy: + return self._x.copy(), self._y.copy() + + return self._x, self._y def as_type(self, dtype: Dtype) -> VectorArray2DABC: """ @@ -230,12 +313,16 @@ def as_type(self, dtype: Dtype) -> VectorArray2DABC: def copy(self) -> VectorArray2DABC: """ - Copies vector array + Copies vector array & meta data """ - return VectorArray2D(self._x.copy(), self._y.copy()) + return VectorArray2D( + x = self._x.copy(), + y = self._y.copy(), + meta = {key: value.copy() for key, value in self._meta.items()}, + ) - def update_from_vector(self, other: VectorArray2DABC): + def update_from_vectorarray(self, other: VectorArray2DABC): """ Updates vector components with data from another vector array @@ -254,6 +341,14 @@ def dtype(self) -> np.dtype: return self._x.dtype + @property + def ndim(self) -> int: + """ + Number of dimensions + """ + + return 2 + @property def mag(self) -> np.ndarray: """ @@ -297,7 +392,7 @@ def y(self, value: float): raise NotImplementedError() @classmethod - def from_iterable(cls, obj: VectorIterable2D, dtype: Dtype = FLOAT_DEFAULT) -> VectorArray2DABC: + def from_iterable(cls, obj: Iterable[Vector2D], dtype: Dtype = FLOAT_DEFAULT) -> VectorArray2DABC: """ Generates vector array object from an iterable of :class:`bewegung.Vector2D` objects @@ -308,27 +403,43 @@ def from_iterable(cls, obj: VectorIterable2D, dtype: Dtype = FLOAT_DEFAULT) -> V if not isinstance(obj, list): obj = list(obj) + x = np.zeros((len(obj),), dtype = dtype) y = np.zeros((len(obj),), dtype = dtype) + keys = set() for idx, item in enumerate(obj): x[idx], y[idx] = item.x, item.y - return cls(x = x, y = y,) + keys.update(item.meta.keys()) + + meta = { + key: np.array([item.meta.get(key) for item in obj]) + for key in keys + } + + return cls(x = x, y = y, meta = meta,) @classmethod - def from_polar(cls, radius: np.ndarray, angle: np.ndarray) -> VectorArray2DABC: + def from_polar(cls, radius: np.ndarray, angle: np.ndarray, meta: Union[MetaArrayDict, None] = None) -> VectorArray2DABC: """ Generates vector array object from arrays of polar vector components Args: radius : Radius components angle : Angle components in radians + meta : A dict holding arbitrary metadata """ - assert radius.ndim == 1 - assert angle.ndim == 1 - assert radius.shape[0] == angle.shape[0] - assert radius.dtype == angle.dtype + if radius.ndim != 1: + raise ValueError('inconsistent: radius.ndim != 1') + if angle.ndim != 1: + raise ValueError('inconsistent: angle.ndim != 1') + if radius.shape[0] != angle.shape[0]: + raise ValueError('inconsistent shape') + if radius.dtype != angle.dtype: + raise ValueError('inconsistent dtype') + x, y = np.cos(angle), np.sin(angle) np.multiply(x, radius, out = x) np.multiply(y, radius, out = y) - return cls(x = x, y = y,) + + return cls(x = x, y = y, meta = meta,) diff --git a/src/bewegung/core/vector/array3d.py b/src/bewegung/linalg/_array3d.py similarity index 54% rename from src/bewegung/core/vector/array3d.py rename to src/bewegung/linalg/_array3d.py index 9e77c15..c756cf6 100644 --- a/src/bewegung/core/vector/array3d.py +++ b/src/bewegung/linalg/_array3d.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/vector/array3d.py: 3D Vector Array + src/bewegung/linalg/_array3d.py: 3D Vector Array Copyright (C) 2020-2021 Sebastian M. Ernst @@ -28,22 +28,29 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from typing import List, Tuple, Union - -import numpy as np -from typeguard import typechecked - -from .lib import dtype_np2py -from .single3d import Vector3D -from ..abc import Dtype, Number, VectorArray3DABC, VectorIterable3D -from ..const import FLOAT_DEFAULT +from collections.abc import Iterable +from numbers import Number +from typing import Any, List, Tuple, Union + +from ..lib import typechecked +from ._abc import ( + Dtype, + MetaArrayDict, + NotImplementedType, + VectorArray3DABC, +) +from ._array import VectorArray +from ._const import FLOAT_DEFAULT +from ._lib import dtype_np2py, dtype_name +from ._numpy import np +from ._single3d import Vector3D # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @typechecked -class VectorArray3D(VectorArray3DABC): +class VectorArray3D(VectorArray, VectorArray3DABC): """ An array of vectors in 3D space. @@ -53,24 +60,38 @@ class VectorArray3D(VectorArray3DABC): x : x components. Must have the same dtype like ``y`` and ``z``. y : y components. Must have the same dtype like ``x`` and ``z``. z : z components. Must have the same dtype like ``x`` and ``y``. + meta : A dict holding arbitrary metadata. """ + def __init__(self, x: np.ndarray, y: np.ndarray, z: np.ndarray, dtype: Union[Dtype, None] = None, meta: Union[MetaArrayDict, None] = None): + + if x.ndim != 1: + raise ValueError('inconsistent: x.ndim != 1') + if y.ndim != 1: + raise ValueError('inconsistent: x.ndim != 1') + if z.ndim != 1: + raise ValueError('inconsistent: z.ndim != 1') + if not x.shape[0] == y.shape[0] == z.shape[0]: + raise ValueError('inconsistent length') + + if dtype is None: + if x.dtype != y.dtype: + raise TypeError('can not guess dtype - inconsistent') + else: + x = x if x.dtype == np.dtype(dtype) else x.astype(dtype) + y = y if y.dtype == np.dtype(dtype) else y.astype(dtype) + z = z if z.dtype == np.dtype(dtype) else z.astype(dtype) - def __init__(self, x: np.ndarray, y: np.ndarray, z: np.ndarray): - - assert x.ndim == 1 - assert y.ndim == 1 - assert z.ndim == 1 - assert x.shape[0] == y.shape[0] == z.shape[0] - assert x.dtype == y.dtype == z.dtype self._x, self._y, self._z = x, y, z + self._iterstate = 0 + super().__init__(meta = meta) def __repr__(self) -> str: """ String representation for interactive use """ - return f'' + return f'' def __len__(self) -> int: """ @@ -83,7 +104,7 @@ def __getitem__(self, idx: Union[int, slice]) -> Union[Vector3D, VectorArray3DAB """ Item access, returning an independent object - either a :class:`bewegung.Vector3D` (index access) or - a :class:`bewegung.VectorArray§D` (slicing) + a :class:`bewegung.VectorArray3D` (slicing) Args: idx : Either an index or a slice @@ -91,11 +112,43 @@ def __getitem__(self, idx: Union[int, slice]) -> Union[Vector3D, VectorArray3DAB if isinstance(idx, int): dtype = dtype_np2py(self.dtype) - return Vector3D(dtype(self._x[idx]), dtype(self._y[idx]), dtype(self._z[idx]), dtype = dtype) + return Vector3D( + x = dtype(self._x[idx]), + y = dtype(self._y[idx]), + z = dtype(self._z[idx]), + dtype = dtype, + meta = {key: value[idx] for key, value in self._meta.items()}, + ) + + return VectorArray3D( + x = self._x[idx].copy(), + y = self._y[idx].copy(), + z = self._z[idx].copy(), + meta = {key: value[idx].copy() for key, value in self._meta.items()}, + ) + + def __iter__(self) -> VectorArray3DABC: + """ + Iterator interface (1/2) + """ + + self._iterstate = 0 + return self + + def __next__(self) -> Vector3D: + """ + Iterator interface (2/2) + """ + + if self._iterstate == len(self): + self._iterstate = 0 # reset + raise StopIteration() - return VectorArray3D(self._x[idx].copy(), self._y[idx].copy(), self._z[idx].copy()) + value = self[self._iterstate] + self._iterstate += 1 # increment + return value - def __eq__(self, other: VectorArray3DABC) -> bool: + def __eq__(self, other: Any) -> Union[bool, NotImplementedType]: """ Equality check between vector arrays @@ -103,9 +156,12 @@ def __eq__(self, other: VectorArray3DABC) -> bool: other : Another vector array of equal length """ + if not isinstance(other, VectorArray3DABC): + return NotImplemented + return np.array_equal(self.x, other.x) and np.array_equal(self.y, other.y) and np.array_equal(self.z, other.z) - def __mod__(self, other: VectorArray3DABC) -> bool: + def __mod__(self, other: Any) -> Union[bool, NotImplementedType]: """ Is-close check between vector arrays @@ -113,9 +169,12 @@ def __mod__(self, other: VectorArray3DABC) -> bool: other : Another vector array of equal length """ + if not isinstance(other, VectorArray3DABC): + return NotImplemented + return np.allclose(self.x, other.x) and np.allclose(self.y, other.y) and np.allclose(self.z, other.z) - def __add__(self, other: Union[VectorArray3DABC, Vector3D]) -> VectorArray3DABC: + def __add__(self, other: Any) -> Union[VectorArray3DABC, NotImplementedType]: """ Add operation between vector arrays or a vector array and a vector @@ -123,16 +182,22 @@ def __add__(self, other: Union[VectorArray3DABC, Vector3D]) -> VectorArray3DABC: other : Another vector array of equal length """ + if not any(isinstance(other, t) for t in (VectorArray3DABC, Vector3D)): + return NotImplemented + if isinstance(other, VectorArray3DABC): - assert len(self) == len(other) - assert self.dtype == other.dtype + if len(self) != len(other): + raise ValueError('inconsistent length') + if self.dtype != other.dtype: + raise TypeError('inconsistent dtype') + return VectorArray3D(self.x + other.x, self.y + other.y, self.z + other.z) def __radd__(self, *args, **kwargs): return self.__add__(*args, **kwargs) - def __sub__(self, other: Union[VectorArray3DABC, Vector3D]) -> VectorArray3DABC: + def __sub__(self, other: Any) -> Union[VectorArray3DABC, NotImplementedType]: """ Substract operator between vector arrays or a vector array and a vector @@ -140,16 +205,22 @@ def __sub__(self, other: Union[VectorArray3DABC, Vector3D]) -> VectorArray3DABC: other : Another vector array of equal length """ + if not any(isinstance(other, t) for t in (VectorArray3DABC, Vector3D)): + return NotImplemented + if isinstance(other, VectorArray3DABC): - assert len(self) == len(other) - assert self.dtype == other.dtype + if len(self) != len(other): + raise ValueError('inconsistent length') + if self.dtype != other.dtype: + raise TypeError('inconsistent dtype') + return VectorArray3D(self.x - other.x, self.y - other.y, self.z - other.z) def __rsub__(self, *args, **kwargs): return self.__sub__(*args, **kwargs) - def __mul__(self, other: Number) -> VectorArray3DABC: + def __mul__(self, other: Any) -> Union[VectorArray3DABC, NotImplementedType]: """ Multiplication with scalar @@ -157,8 +228,15 @@ def __mul__(self, other: Number) -> VectorArray3DABC: other : A number """ + if not isinstance(other, Number): + return NotImplemented + return VectorArray3D(self._x * other, self._y * other, self._z * other) + def __rmul__(self, *args, **kwargs): + + return self.__mul__(*args, **kwargs) + def mul(self, scalar: Number): """ In-place multiplication with scalar @@ -171,7 +249,7 @@ def mul(self, scalar: Number): np.multiply(self._y, scalar, out = self._y) np.multiply(self._z, scalar, out = self._z) - def __matmul__(self, other: VectorArray3DABC) -> np.ndarray: + def __matmul__(self, other: Any) -> Union[np.ndarray, NotImplementedType]: """ Scalar product between vector arrays @@ -179,8 +257,14 @@ def __matmul__(self, other: VectorArray3DABC) -> np.ndarray: other : Another vector array of equal length """ - assert len(self) == len(other) - assert self.dtype == other.dtype + if not isinstance(other, VectorArray3DABC): + return NotImplemented + + if len(self) != len(other): + raise ValueError('inconsistent length') + if self.dtype != other.dtype: + raise TypeError('inconsistent dtype') + return self.x * other.x + self.y * other.y + self.z * other.z def as_list(self) -> List[Vector3D]: @@ -188,11 +272,7 @@ def as_list(self) -> List[Vector3D]: Exports a list of :class:`bewegung.Vector3D` objects """ - dtype = dtype_np2py(self.dtype) - return [ - Vector3D(dtype(self._x[idx]), dtype(self._y[idx]), dtype(self._z[idx]), dtype = dtype) - for idx in range(len(self)) - ] + return list(self) def as_ndarray(self, dtype: Dtype = FLOAT_DEFAULT) -> np.ndarray: """ @@ -213,12 +293,26 @@ def as_polar_tuple(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: return (self.mag, self.theta, self.phi) - def as_tuple(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + def as_geographic_tuple(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Exports vector array as a tuple of geographic coordinate components + in ``numpy.ndarry`` objects (radius, lon, lat) + """ + + return (self.mag, self.lon, self.lat) + + def as_tuple(self, copy: bool = True) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Exports vector array as a tuple of vector components in ``numpy.ndarry`` objects + + Args: + copy : Provide a copy of underlying ``numpy.ndarry`` """ - return self._x.copy(), self._y.copy(), self._z.copy() + if copy: + return self._x.copy(), self._y.copy(), self._z.copy() + + return self._x, self._y, self._z def as_type(self, dtype: Dtype) -> VectorArray3DABC: """ @@ -234,12 +328,17 @@ def as_type(self, dtype: Dtype) -> VectorArray3DABC: def copy(self) -> VectorArray3DABC: """ - Copies vector array + Copies vector array & meta data """ - return VectorArray3D(self._x.copy(), self._y.copy(), self._z.copy()) + return VectorArray3D( + x = self._x.copy(), + y = self._y.copy(), + z = self._z.copy(), + meta = {key: value.copy() for key, value in self._meta.items()}, + ) - def update_from_vector(self, other: VectorArray3DABC): + def update_from_vectorarray(self, other: VectorArray3DABC): """ Updates vector components with data from another vector array @@ -259,6 +358,14 @@ def dtype(self) -> np.dtype: return self._x.dtype + @property + def ndim(self) -> int: + """ + Number of dimensions + """ + + return 3 + @property def mag(self) -> np.ndarray: """ @@ -283,6 +390,27 @@ def phi(self) -> np.ndarray: return np.arctan2(self._y, self._x) + @property + def lat(self) -> float: + """ + The vectors' geographic latitude in degree, computed on demand + """ + + rad2deg = self.dtype.type(180.0 / np.pi) + halfpi = self.dtype.type(np.pi / 2.0) + + return -(self.theta - halfpi) * rad2deg + + @property + def lon(self) -> float: + """ + The vectors' gepgraphic longitude in degree, computed on demand + """ + + rad2deg = self.dtype.type(180.0 / np.pi) + + return self.phi * rad2deg + @property def x(self) -> np.ndarray: """ @@ -323,7 +451,7 @@ def z(self, value: float): raise NotImplementedError() @classmethod - def from_iterable(cls, obj: VectorIterable3D, dtype: Dtype = FLOAT_DEFAULT) -> VectorArray3DABC: + def from_iterable(cls, obj: Iterable[Vector3D], dtype: Dtype = FLOAT_DEFAULT) -> VectorArray3DABC: """ Generates vector array object from an iterable of :class:`bewegung.Vector3D` objects @@ -334,15 +462,24 @@ def from_iterable(cls, obj: VectorIterable3D, dtype: Dtype = FLOAT_DEFAULT) -> V if not isinstance(obj, list): obj = list(obj) + x = np.zeros((len(obj),), dtype = dtype) y = np.zeros((len(obj),), dtype = dtype) z = np.zeros((len(obj),), dtype = dtype) + keys = set() for idx, item in enumerate(obj): x[idx], y[idx], z[idx] = item.x, item.y, item.z - return cls(x = x, y = y, z = z,) + keys.update(item.meta.keys()) + + meta = { + key: np.array([item.meta.get(key) for item in obj]) + for key in keys + } + + return cls(x = x, y = y, z = z, meta = meta,) @classmethod - def from_polar(cls, radius: np.ndarray, theta: np.ndarray, phi: np.ndarray) -> VectorArray3DABC: + def from_polar(cls, radius: np.ndarray, theta: np.ndarray, phi: np.ndarray, meta: Union[MetaArrayDict, None] = None) -> VectorArray3DABC: """ Generates vector array object from arrays of polar vector components @@ -350,22 +487,30 @@ def from_polar(cls, radius: np.ndarray, theta: np.ndarray, phi: np.ndarray) -> V radius : Radius components theta : Angle components in radians phi : Angle components in radians + meta : A dict holding arbitrary metadata """ - assert radius.ndim == 1 - assert theta.ndim == 1 - assert phi.ndim == 1 - assert radius.shape[0] == theta.shape[0] == phi.shape[0] - assert radius.dtype == theta.dtype == phi.dtype + if radius.ndim != 1: + raise ValueError('inconsistent: radius.ndim != 1') + if theta.ndim != 1: + raise ValueError('inconsistent: theta.ndim != 1') + if phi.ndim != 1: + raise ValueError('inconsistent: phi.ndim != 1') + if not radius.shape[0] == theta.shape[0] == phi.shape[0]: + raise ValueError('inconsistent shape') + if not radius.dtype == theta.dtype == phi.dtype: + raise ValueError('inconsistent dtype') + RadiusSinTheta = radius * np.sin(theta) return cls( x = RadiusSinTheta * np.cos(phi), y = RadiusSinTheta * np.sin(phi), z = radius * np.cos(theta), + meta = meta, ) @classmethod - def from_geographic(cls, radius: np.ndarray, lon: np.ndarray, lat: np.ndarray) -> VectorArray3DABC: + def from_geographic(cls, radius: np.ndarray, lon: np.ndarray, lat: np.ndarray, meta: Union[MetaArrayDict, None] = None) -> VectorArray3DABC: """ Generates vector array object from arrays of geographic polar vector components @@ -373,17 +518,25 @@ def from_geographic(cls, radius: np.ndarray, lon: np.ndarray, lat: np.ndarray) - radius : Radius components lon : Angle components in degree lat : Angle components in degree + meta : A dict holding arbitrary metadata """ - assert radius.ndim == 1 - assert lon.ndim == 1 - assert lat.ndim == 1 - assert radius.shape[0] == lon.shape[0] == lat.shape[0] - assert radius.dtype == lon.dtype == lat.dtype - rad2deg = np.dtype(radius.dtype).type(np.pi / 180.0) + if radius.ndim != 1: + raise ValueError('inconsistent: radius.ndim != 1') + if lon.ndim != 1: + raise ValueError('inconsistent: lon.ndim != 1') + if lat.ndim != 1: + raise ValueError('inconsistent: lat.ndim != 1') + if not radius.shape[0] == lon.shape[0] == lat.shape[0]: + raise ValueError('inconsistent shape') + if not radius.dtype == lon.dtype == lat.dtype: + raise ValueError('inconsistent dtype') + + deg2rad = np.dtype(radius.dtype).type(np.pi / 180.0) halfpi = np.dtype(radius.dtype).type(np.pi / 2.0) return cls.from_polar( radius = radius, - theta = halfpi - (lat * rad2deg), - phi = lon * rad2deg, + theta = halfpi - (lat * deg2rad), + phi = lon * deg2rad, + meta = meta, ) diff --git a/src/bewegung/core/camera.py b/src/bewegung/linalg/_camera.py similarity index 95% rename from src/bewegung/core/camera.py rename to src/bewegung/linalg/_camera.py index 5f5afad..943670a 100644 --- a/src/bewegung/core/camera.py +++ b/src/bewegung/linalg/_camera.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/camera.py: Simple pinhole camera + src/bewegung/linalg/_camera.py: Simple pinhole camera Copyright (C) 2020-2021 Sebastian M. Ernst @@ -32,11 +32,6 @@ import sys from typing import Union -try: - import numpy as np -except ModuleNotFoundError: - np = None - try: from numba import jit, float32, float64, boolean except ModuleNotFoundError: @@ -46,16 +41,14 @@ def wrapper(func): return wrapper boolean, float32, float64 = None, tuple(), tuple() -from .abc import CameraABC -from .typeguard import typechecked -from .vector import ( - Matrix, - Vector2D, - Vector2Ddist, - Vector3D, - VectorArray2Ddist, - VectorArray3D, - ) +from ..lib import typechecked +from ._abc import CameraABC +from ._matrix import Matrix +from ._numpy import np +from ._single2d import Vector2D +from ._single3d import Vector3D +from ._array2d import VectorArray2D +from ._array3d import VectorArray3D # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -255,10 +248,10 @@ def planeYFlip(self, value: bool): self._planeYFlip = value - def get_point(self, point3D: Vector3D) -> Vector2Ddist: + def get_point(self, point3D: Vector3D) -> Vector2D: """ Projects a 3D vector onto a 2D plane. - Returns a 2D vector combined with the absolute distance to the camera in 3D space. + Returns a 2D vector combined with the absolute distance to the camera in 3D space (``meta["dist"]``). Args: point3D : point in 3D space @@ -308,16 +301,16 @@ def get_point(self, point3D: Vector3D) -> Vector2Ddist: point2D.y *= self._planeFactor point2D += self._planeOffset - return Vector2Ddist( + return Vector2D( x = point2D.x, y = point2D.y, - dist = (point3D - self._position).mag, + meta = dict(dist = (point3D - self._position).mag), ) - def get_points(self, points3d: VectorArray3D) -> VectorArray2Ddist: + def get_points(self, points3d: VectorArray3D) -> VectorArray2D: """ Projects a 3D vector array onto a 2D plane. - Returns a 2D vector array combined with the absolute distances to the camera in 3D space. + Returns a 2D vector array combined with the absolute distances to the camera in 3D space (``meta["dist"]``). Args: points3d : points in 3D space @@ -349,10 +342,10 @@ def get_points(self, points3d: VectorArray3D) -> VectorArray2Ddist: planeFactor, self._planeYFlip, ) - return VectorArray2Ddist( + return VectorArray2D( x = points2d[:, 0], y = points2d[:, 1], - dist = points2d[:, 2], + meta = dict(dist = points2d[:, 2]), ) @staticmethod diff --git a/src/bewegung/linalg/_const.py b/src/bewegung/linalg/_const.py new file mode 100644 index 0000000..2d4fc0e --- /dev/null +++ b/src/bewegung/linalg/_const.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/linalg/_const.py: Const values + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CONST +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +FLOAT_DEFAULT = 'f4' diff --git a/src/bewegung/core/vector/lib.py b/src/bewegung/linalg/_lib.py similarity index 75% rename from src/bewegung/core/vector/lib.py rename to src/bewegung/linalg/_lib.py index f8f26c0..d2ddd78 100644 --- a/src/bewegung/core/vector/lib.py +++ b/src/bewegung/linalg/_lib.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/vector/lib.py: Vector library + src/bewegung/linalg/_lib.py: Linear algebra library Copyright (C) 2020-2021 Sebastian M. Ernst @@ -28,19 +28,19 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from typing import Type - -import numpy as np -from typeguard import typechecked - -from ..abc import Dtype +from ..lib import typechecked +from ._abc import Dtype, NumberType +from ._numpy import np # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# CLASS +# ROUTINES # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @typechecked -def dtype_np2py(dtype: Dtype) -> Type: +def dtype_np2py(dtype: Dtype) -> NumberType: + """ + Map numpy dtypes to Python number types + """ if np.issubdtype(dtype, np.integer): return int @@ -48,3 +48,14 @@ def dtype_np2py(dtype: Dtype) -> Type: return float else: raise TypeError("numpy dtype can not be mapped on Python number types") + +@typechecked +def dtype_name(dtype: Dtype) -> str: + """ + Provides name of both Python and numpy number/array types + """ + + return getattr( + dtype, '__name__', + str(dtype), # fallback, numpy + ) diff --git a/src/bewegung/linalg/_matrix.py b/src/bewegung/linalg/_matrix.py new file mode 100644 index 0000000..e6c4c11 --- /dev/null +++ b/src/bewegung/linalg/_matrix.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/linalg/_matrix.py: Simple 2x2/3x3 matrix for rotations + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from collections.abc import Iterable +from math import cos, sin, isclose +from numbers import Number +from typing import Any, Tuple, Union + +from ..lib import typechecked +from ._abc import ( + Dtype, + MatrixABC, + MetaDict, + NotImplementedType, + Numbers, + NumberType, +) +from ._array import VectorArray +from ._array2d import VectorArray2D +from ._array3d import VectorArray3D +from ._const import FLOAT_DEFAULT +from ._lib import dtype_name +from ._numpy import np, ndarray +from ._single import Vector +from ._single2d import Vector2D +from ._single3d import Vector3D + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class Matrix(MatrixABC): + """ + A simple matrix implementation for transforming vectors and vector arrays + + Mutable. + + Args: + matrix : 2D or 3D arrangement in a list of lists containing Python numbers + dtype : Data type. Derived from entries in ``matrix`` if not explicitly provided. + meta : A dict holding arbitrary metadata + """ + + def __init__(self, matrix = Iterable[Iterable[Numbers]], dtype: Union[NumberType, None] = None, meta: Union[MetaDict, None] = None): + + matrix = [list(row) for row in matrix] # convert to lists or copy lists + + rows = len(matrix) + if rows not in (2, 3): # allow 2D and 3D + raise ValueError('neither 2D nor 3D') + if not all((len(row) == rows for row in matrix)): # check columns + raise ValueError('number of rows and columns do not match') + + if dtype is None: + dtype = type(matrix[0][0]) + if not all(all(isinstance(col, dtype) for col in row) for row in matrix): + raise TypeError('can not guess dtype - inconsistent') + else: + matrix = [[dtype(col) for col in row] for row in matrix] + + self._matrix = matrix + self._meta = {} if meta is None else dict(meta) + + def __repr__(self) -> str: + """ + String representation for interactive use + """ + + values = ',\n'.join([ + f' ({", ".join([str(col) for col in row]):s})' for row in self._matrix + ]) + + return f'' + + def __matmul__(self, other: Any) -> Union[Vector, VectorArray, NotImplementedType]: + """ + Multiplies the matrix with a vector or array of vectors + and returns the resulting new vector or array of vectors. + Raises an exception if matrix and vector or + array of vectors have different numbers of dimensions. + + Args: + other : A 2D or 3D vector or array of vectors + """ + + if not any(isinstance(other, t) for t in (Vector, VectorArray)): + return NotImplemented + + if self.ndim != other.ndim: + raise ValueError('dimension mismatch') + + vector_tuple = other.as_tuple(copy = False) if isinstance(other, VectorArray) else other.as_tuple() + + if isinstance(other, VectorArray) and np is not None: + sum_ = lambda x: np.sum(np.array(x), axis = 0) + else: + sum_ = sum + + values = [ + sum_([ + matrix_element * vector_coordinate + for matrix_element, vector_coordinate in zip(matrix_row, vector_tuple) + ]) + for matrix_row in self._matrix + ] + + if isinstance(other, Vector): + return Vector2D(*values) if len(vector_tuple) == 2 else Vector3D(*values) + + return VectorArray2D(*values) if len(vector_tuple) == 2 else VectorArray3D(*values) + + def __getitem__(self, index: Tuple[int, int]) -> Number: + """ + Item access, returns value at position + + Args: + index : Row and column index + """ + + return self._matrix[index[0]][index[1]] + + def __setitem__(self, index: Tuple[int, int], value: Number): + """ + Item access, sets new value at position + + Args: + index : Row and column index + value : New value + """ + + self._matrix[index[0]][index[1]] = self.dtype(value) + + def __eq__(self, other: Any) -> Union[bool, NotImplementedType]: + """ + Equality check between matrices + + Args: + other : Another matrix + """ + + if not isinstance(other, MatrixABC): + return NotImplemented + + if self.ndim != other.ndim: + return False + + return self.as_tuple() == other.as_tuple() + + def __mod__(self, other: Any) -> Union[bool, NotImplementedType]: + """ + Is-close check between matrices + + Args: + other : Another matrix + """ + + if not isinstance(other, MatrixABC): + return NotImplemented + + if self.ndim != other.ndim: + return False + + return all( + isclose(number_a, number_b) + for line_a, line_b in zip(self.as_tuple(), other.as_tuple()) + for number_a, number_b in zip(line_a, line_b) + ) + + def as_ndarray(self, dtype: Dtype = FLOAT_DEFAULT) -> ndarray: + """ + Exports matrix as a ``numpy.ndarry`` object, shape ``(2, 2)`` or ``(3, 3)``. + + Args: + dtype : Desired ``numpy`` data type of new vector + """ + + if np is None: + raise NotImplementedError('numpy is not available') + + return np.array(self._matrix, dtype = dtype) + + def as_tuple(self) -> Tuple[Tuple[Numbers, ...], ...]: + """ + Exports matrix as a tuple of tuples + """ + + return tuple(tuple(item) for item in self._matrix) + + def copy(self) -> MatrixABC: + """ + Copies matrix & meta data + """ + + return type(self)(matrix = [row.copy() for row in self._matrix], dtype = self.dtype, meta = self._meta.copy()) + + @property + def dtype(self) -> NumberType: + """ + (Python) data type of matrix components + """ + + return type(self._matrix[0][0]) + + @property + def ndim(self) -> int: + """ + Number of dimensions, either ``2`` or ``3``. + """ + + return len(self._matrix) + + @property + def meta(self) -> MetaDict: + """ + meta data dict + """ + + return self._meta + + @classmethod + def from_ndarray(cls, matrix: ndarray, dtype: NumberType = float, meta: Union[MetaDict, None] = None) -> MatrixABC: + """ + Generates new matrix object from ``numpy.ndarray`` object + of shape ``(2, 2)`` or ``(3, 3)`` + + Args: + matrix : Input data + dtype : Desired (Python) data type of matrix + meta : A dict holding arbitrary metadata + """ + + if matrix.ndim != 2: + raise ValueError('shape mismatch - ndim != 2') + if matrix.shape not in ((2, 2), (3, 3)): + raise ValueError('shape mismatch - not NxN') + + matrix = [[dtype(col) for col in row] for row in matrix.tolist()] + + return cls(matrix, dtype = dtype, meta = meta,) + + @classmethod + def from_2d_rotation(cls, a: Number, meta: Union[MetaDict, None] = None) -> MatrixABC: + """ + Generates new 2D matrix object from an angle + + Args: + a : An angle in radians + meta : A dict holding arbitrary metadata + """ + + sa, ca = sin(a), cos(a) + + return cls( + [ + [ca, -sa], + [sa, ca], + ], + meta = meta, + ) + + @classmethod + def from_3d_rotation(cls, v: Vector3D, a: Number, meta: Union[MetaDict, None] = None) -> MatrixABC: + """ + Generates new 3D matrix object from a vector and an angle. + Rotates by angle around vector. + + Args: + v : A 3D vector + a : An angle in radians + meta : A dict holding arbitrary metadata + """ + + ca = cos(a) + oca = 1 - ca + sa = sin(a) + + return cls( + [ + [ca + (v.x ** 2) * oca, v.x * v.y * oca - v.z * sa, v.x * v.y * oca + v.y * sa], + [v.y * v.x * oca + v.z * sa, ca + (v.y ** 2) * oca, v.y * v.z * oca - v.x * sa], + [v.z * v.x * oca - v.y * sa, v.z * v.y * oca + v.x * sa, ca + (v.z ** 2) * oca], + ], + meta = meta, + ) diff --git a/src/bewegung/linalg/_matrixarray.py b/src/bewegung/linalg/_matrixarray.py new file mode 100644 index 0000000..1dbc8f8 --- /dev/null +++ b/src/bewegung/linalg/_matrixarray.py @@ -0,0 +1,449 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/linalg/_matrixarray.py: Array of simple 2x2/3x3 matrices for rotations + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from collections.abc import Iterable +from numbers import Number +from typing import Any, List, Tuple, Union + +from ..lib import typechecked +from ._abc import ( + Dtype, + MatrixArrayABC, + MetaArrayDict, + NotImplementedType, +) +from ._const import FLOAT_DEFAULT +from ._array import VectorArray +from ._lib import dtype_np2py, dtype_name +from ._numpy import np, ndarray +from ._single import Vector +from ._single3d import Vector3D +from ._array2d import VectorArray2D +from ._array3d import VectorArray3D +from ._matrix import Matrix + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class MatrixArray(MatrixArrayABC): + """ + An array implementation of simple matrices for transforming vectors and vector arrays + + Mutable. + + Args: + matrix : 2D or 3D arrangement in a list of lists containing numpy nd arrays + """ + + def __init__(self, matrix = Iterable[Iterable[ndarray]], dtype: Union[Dtype, None] = None, meta: Union[MetaArrayDict, None] = None): + + matrix = [list(row) for row in matrix] # convert to lists or copy lists + + rows = len(matrix) + if rows not in (2, 3): # allow 2D and 3D + raise ValueError('dimension mismatch - neither 2D nor 3D') + if not all((len(row) == rows for row in matrix)): + raise ValueError('inconsistent rows') + + if not all(col.ndim == 1 for row in matrix for col in row): + raise ValueError('inconsistent: ndarray.ndim != 1') + + length = matrix[0][0].shape[0] + if not all(col.shape[0] == length for row in matrix for col in row): + raise ValueError('inconsistent length') + + if dtype is None: + dtype = matrix[0][0].dtype + if not all(col.dtype == dtype for row in matrix for col in row): + raise TypeError('can not guess dtype - inconsistent') + else: + dtype = np.dtype(dtype) + matrix = [ + [ + col if col.dtype == dtype else col.astype(dtype) + for col in row + ] + for row in matrix + ] + + self._matrix = matrix + self._iterstate = 0 + + meta = {} if meta is None else dict(meta) + + if not all(value.ndim == 1 for value in meta.values()): + raise ValueError('inconsistent: meta_value.ndim != 1') + if not all(value.shape[0] == len(self) for value in meta.values()): + raise ValueError('inconsistent length') + + self._meta = meta + + def __repr__(self) -> str: + """ + String representation for interactive use + """ + + return f'' + + def __len__(self) -> int: + """ + Length of array + """ + + return self._matrix[0][0].shape[0] + + def __matmul__(self, other: Any) -> Union[VectorArray, NotImplementedType]: + """ + Multiplies the matrix array with a vector or array of vectors + and returns the resulting new vector or array of vectors. + Raises an exception if matrix and vector or + array of vectors have different numbers of dimensions. + + Args: + vector : A 2D or 3D vector or array of vectors + """ + + if not any(isinstance(other, t) for t in (Vector, VectorArray)): + return NotImplemented + + if self.ndim != other.ndim: + raise ValueError('dimension mismatch') + + if len(self) > 1 and isinstance(other, VectorArray): + if len(other) > 1 and len(self) != len(other): + raise ValueError('length mismatch') + + vector_tuple = other.as_tuple(copy = False) if isinstance(other, VectorArray) else other.as_tuple() + + values = [ + np.sum(np.array([ + matrix_element * vector_coordinate + for matrix_element, vector_coordinate in zip(matrix_row, vector_tuple) + ]), axis = 0) + for matrix_row in self._matrix + ] + + return VectorArray2D(*values) if len(vector_tuple) == 2 else VectorArray3D(*values) + + def __getitem__(self, idx: Union[int, slice]) -> Union[Matrix, MatrixArrayABC]: + """ + Item access, returns value at position + + Args: + index : Row, column and position index + """ + + if isinstance(idx, slice): + return MatrixArray( + matrix = [ + [col[idx].copy() for col in row] + for row in self._matrix + ], + meta = {key: value[idx].copy() for key, value in self._meta.items()}, + ) + + dtype = dtype_np2py(self.dtype) + + return Matrix( + matrix = [ + [dtype(col[idx]) for col in row] + for row in self._matrix + ], + meta = {key: value[idx] for key, value in self._meta.items()}, + ) + + def __iter__(self) -> MatrixArrayABC: + """ + Iterator interface (1/2) + """ + + self._iterstate = 0 + return self + + def __next__(self) -> Matrix: + """ + Iterator interface (2/2) + """ + + if self._iterstate == len(self): + self._iterstate = 0 # reset + raise StopIteration() + + value = self[self._iterstate] + self._iterstate += 1 # increment + return value + + def __eq__(self, other: Any) -> Union[bool, NotImplementedType]: + """ + Equality check between matrix arrays + + Args: + other : Another matrix array of equal length + """ + + if not isinstance(other, MatrixArrayABC): + return NotImplemented + + if self.ndim != other.ndim or len(self) != len(other): + return False + + return all( + np.array_equal(self_col, other_col) + for self_row, other_row in zip(self._matrix, other._matrix) + for self_col, other_col in zip(self_row, other_row) + ) + + def __mod__(self, other: Any) -> Union[bool, NotImplementedType]: + """ + Is-close check between matrix arrays + + Args: + other : Another matrix array of equal length + """ + + if not isinstance(other, MatrixArrayABC): + return NotImplemented + + if self.ndim != other.ndim or len(self) != len(other): + return False + + return all( + np.allclose(self_col, other_col) + for self_row, other_row in zip(self._matrix, other._matrix) + for self_col, other_col in zip(self_row, other_row) + ) + + def as_list(self) -> List[Matrix]: + """ + Exports a list of :class:`bewegung.Matrix` objects + """ + + return list(self) + + def as_ndarray(self, dtype: Dtype = FLOAT_DEFAULT) -> ndarray: + """ + Exports matrix array as a ``numpy.ndarry`` object, shape ``(len(self), self.ndim, self.ndim)``. + + Args: + dtype : Desired ``numpy`` data type of new vector + """ + + return np.moveaxis(np.array(self._matrix, dtype = dtype), 2, 0) + + def as_tuple(self, copy: bool = True) -> Tuple[Tuple[ndarray, ...], ...]: + """ + Exports matrix array as a tuple of tuples of ``numpy.ndarray`` objects + + Args: + copy : Provide a copy of underlying ``numpy.ndarry`` + """ + + if copy: + return tuple(tuple(col.copy() for col in row) for row in self._matrix) + + return tuple(tuple(col for col in row) for row in self._matrix) + + def as_type(self, dtype: Dtype) -> MatrixArrayABC: + """ + Exports matrix array as another matrix array with new dtype + + Args: + dtype : Desired ``numpy`` data type of new vector array + """ + + return self.copy() if self.dtype == np.dtype(dtype) else Matrix([ + [col.astype(dtype) for col in row] for row in self._matrix + ]) + + def copy(self) -> MatrixArrayABC: + """ + Copies matrix array & meta data + """ + + return type(self)( + matrix = [ + [col.copy() for col in row] + for row in self._matrix + ], + meta = {key: value.copy() for key, value in self._meta.items()}, + ) + + @property + def dtype(self) -> np.dtype: + """ + (Python) data type of matrix components + """ + + return self._matrix[0][0].dtype + + @property + def ndim(self) -> int: + """ + Number of dimensions, either ``2`` or ``3``. + """ + + return len(self._matrix) + + @property + def meta(self) -> MetaArrayDict: + """ + meta data dict + """ + + return self._meta + + @classmethod + def from_iterable(cls, obj: Iterable[Matrix], dtype: Dtype = FLOAT_DEFAULT) -> MatrixArrayABC: + """ + Generates matrix array object from an iterable of :class:`bewegung.Matrix` objects + + Args: + obj : iterable + dtype : Desired ``numpy`` data type of new vector array + """ + + if not isinstance(obj, list): + obj = list(obj) + + ndim = obj[0].ndim + if not all(item.ndim == ndim for item in obj): + raise ValueError('inconsistent ndim') + + matrix = [ + [ + np.zeros((len(obj),), dtype = dtype) + for __ in range(len(obj)) + ] + for _ in range(len(obj)) + ] + + keys = set() + for idx, item in enumerate(obj): + for row in range(ndim): + for col in range(ndim): + matrix[row][col][idx] = item[row][col] + keys.update(item.meta.keys()) + + meta = { + key: np.array([item.meta.get(key) for item in obj]) + for key in keys + } + + return cls(matrix = matrix, meta = meta,) + + @classmethod + def from_ndarray(cls, matrix_array: ndarray, meta: Union[MetaArrayDict, None] = None) -> MatrixArrayABC: + """ + Generates new matrix array object from single ``numpy.ndarray`` + object of shape ``(length, ndim, ndim)`` + + Args: + matrix_array : Input data + """ + + if matrix_array.ndim != 3: + raise ValueError('dimension mismatch: ndim != 3)') + if matrix_array.shape[1:] not in ((2, 2), (3, 3)): + raise ValueError('dimension mismatch: not 2x2 or 3x3)') + + ndim = matrix_array.shape[1] + + return cls( + matrix = [ + [matrix_array[:, row, col] for col in range(ndim)] + for row in range(ndim) + ], + meta = meta, + ) + + @classmethod + def from_2d_rotation(cls, a: ndarray, meta: Union[MetaArrayDict, None] = None) -> MatrixArrayABC: + """ + Generates new 2D matrix array object from an array of angles + + Args: + a : An array of angles in radians + """ + + if a.ndim != 1: + raise ValueError('dimension mismatch') + + sa, ca = np.sin(a), np.cos(a) + + return cls( + matrix = [ + [ca, -sa], + [sa, ca.copy()], + ], + meta = meta, + ) + + @classmethod + def from_3d_rotation( + cls, + v: Union[Vector3D, VectorArray3D], + a: Union[Number, ndarray], + meta: Union[MetaArrayDict, None] = None, + ) -> MatrixArrayABC: + """ + Generates new 3D matrix array object from a vector or vector array and + an angle or one-dimensional ``numpy.ndarray`` of angles. + Rotates by angle around vector. + + Args: + v : A 3D vector or vector array + a : An angle or array of angles in radians + """ + + if not isinstance(v, VectorArray3D) and not isinstance(a, ndarray): + raise TypeError('neither v nor a are arrays') + + if isinstance(a, ndarray) and a.ndim != 1: + raise ValueError('shape mismatch') + + if isinstance(v, VectorArray3D) and isinstance(a, ndarray): + if len(v) != 1 and a.shape[0] != 1: + if len(v) != a.shape[0]: + raise ValueError('length mismatch') + + ca = np.cos(a) + oca = 1 - ca + sa = np.sin(a) + + return cls( + matrix = [ + [ca + (v.x ** 2) * oca, v.x * v.y * oca - v.z * sa, v.x * v.y * oca + v.y * sa], + [v.y * v.x * oca + v.z * sa, ca + (v.y ** 2) * oca, v.y * v.z * oca - v.x * sa], + [v.z * v.x * oca - v.y * sa, v.z * v.y * oca + v.x * sa, ca + (v.z ** 2) * oca], + ], + meta = meta, + ) diff --git a/src/bewegung/linalg/_numpy.py b/src/bewegung/linalg/_numpy.py new file mode 100644 index 0000000..157afe4 --- /dev/null +++ b/src/bewegung/linalg/_numpy.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/linalg/_numpy.py: Wrapper around numpy (optional) + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +try: + import numpy as np + from numpy import ndarray +except ModuleNotFoundError: + np, ndarray = None, None diff --git a/src/bewegung/linalg/_single.py b/src/bewegung/linalg/_single.py new file mode 100644 index 0000000..350b93e --- /dev/null +++ b/src/bewegung/linalg/_single.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/linalg/_single.py: Single base class + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from abc import ABC, abstractmethod +from typing import Union + +from ..lib import typechecked +from ._abc import MetaDict + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class Vector(ABC): + """ + Abstract base class for all vector types. + + Not intended to be instantiated. + + Args: + meta : A dict holding arbitrary metadata + """ + + @abstractmethod + def __init__(self, meta: Union[MetaDict, None] = None): + + self._meta = {} if meta is None else dict(meta) + + @property + def meta(self) -> MetaDict: + """ + meta data dict + """ + + return self._meta diff --git a/src/bewegung/core/vector/single2d.py b/src/bewegung/linalg/_single2d.py similarity index 62% rename from src/bewegung/core/vector/single2d.py rename to src/bewegung/linalg/_single2d.py index 8888c28..86dc05f 100644 --- a/src/bewegung/core/vector/single2d.py +++ b/src/bewegung/linalg/_single2d.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/vector/single2d.py: Single 2D Vector + src/bewegung/linalg/_single2d.py: Single 2D Vector Copyright (C) 2020-2021 Sebastian M. Ernst @@ -29,24 +29,30 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import math -from typing import Tuple, Type, Union - -try: - import numpy as np - from numpy import ndarray -except ModuleNotFoundError: - np, ndarray = None, None -from typeguard import typechecked - -from ..abc import Dtype, PyNumber, PyNumber2D, Vector2DABC, VectorArray2DABC -from ..const import FLOAT_DEFAULT +from numbers import Number +from typing import Any, Tuple, Union + +from ..lib import typechecked +from ._abc import ( + Dtype, + NotImplementedType, + MetaDict, + Number2D, + NumberType, + Vector2DABC, +) +from ._const import FLOAT_DEFAULT +from ._lib import dtype_name +from ._numpy import np, ndarray +from ._single import Vector +from ._svg import Svg # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @typechecked -class Vector2D(Vector2DABC): +class Vector2D(Vector, Vector2DABC): """ A single vector in 2D space. @@ -56,28 +62,34 @@ class Vector2D(Vector2DABC): x : x component. Must have the same type like ``y``. y : y component. Must have the same type like ``x``. dtype : Data type. Derived from ``x`` and ``y`` if not explicitly provided. + meta : A dict holding arbitrary metadata. """ - def __init__(self, x: PyNumber, y: PyNumber, dtype: Union[Type, None] = None): + def __init__(self, x: Number, y: Number, dtype: Union[NumberType, None] = None, meta: Union[MetaDict, None] = None): - assert type(x) == type(y) if dtype is None: - dtype = type(x) + if type(x) != type(y): + raise TypeError('can not guess dtype - inconsistent') else: - assert dtype == type(x) + x, y = dtype(x), dtype(y) - self._x, self._y, self._dtype = x, y, dtype + self._x, self._y = x, y + super().__init__(meta = meta) def __repr__(self) -> str: """ String representation for interactive use """ - if self._dtype == int: - return f'' - return f'' + if self.dtype == int: + return f'' + return f'' + + def _repr_svg_(self) -> str: + + return Svg(self).render() - def __eq__(self, other: Vector2DABC) -> bool: + def __eq__(self, other: Any) -> Union[bool, NotImplementedType]: """ Equality check between vectors @@ -85,9 +97,12 @@ def __eq__(self, other: Vector2DABC) -> bool: other : Another vector """ - return (self.x == other.x) and (self.y == other.y) + if not isinstance(other, Vector2DABC): + return NotImplemented - def __mod__(self, other: Vector2DABC) -> bool: + return bool(self.x == other.x) and bool(self.y == other.y) + + def __mod__(self, other: Any) -> Union[bool, NotImplementedType]: """ Is-close check (relevant for dtype ``float``) between vectors @@ -95,9 +110,12 @@ def __mod__(self, other: Vector2DABC) -> bool: other : Another vector """ + if not isinstance(other, Vector2DABC): + return NotImplemented + return math.isclose(self.x, other.x) and math.isclose(self.y, other.y) - def __add__(self, other: Union[Vector2DABC, VectorArray2DABC]) -> Vector2DABC: + def __add__(self, other: Any) -> Union[Vector2DABC, NotImplementedType]: """ Add operation between vectors or a vector and a vector array @@ -105,12 +123,12 @@ def __add__(self, other: Union[Vector2DABC, VectorArray2DABC]) -> Vector2DABC: other : Another vector """ - if isinstance(other, VectorArray2DABC): - return NotImplemented # hand off to array type + if not isinstance(other, Vector2DABC): + return NotImplemented return type(self)(self.x + other.x, self.y + other.y) - def __sub__(self, other: Union[Vector2DABC, VectorArray2DABC]) -> Vector2DABC: + def __sub__(self, other: Any) -> Union[Vector2DABC, NotImplementedType]: """ Substract operator between vectors or a vector and a vector array @@ -118,12 +136,12 @@ def __sub__(self, other: Union[Vector2DABC, VectorArray2DABC]) -> Vector2DABC: other : Another vector """ - if isinstance(other, VectorArray2DABC): - return NotImplemented # hand off to array type + if not isinstance(other, Vector2DABC): + return NotImplemented return type(self)(self.x - other.x, self.y - other.y) - def __mul__(self, other: PyNumber) -> Vector2DABC: + def __mul__(self, other: Any) -> Union[Vector2DABC, NotImplementedType]: """ Multiplication with scalar @@ -131,9 +149,16 @@ def __mul__(self, other: PyNumber) -> Vector2DABC: other : A number """ + if not isinstance(other, Number): + return NotImplemented + return type(self)(self._x * other, self._y * other) - def mul(self, scalar: PyNumber): + def __rmul__(self, *args, **kwargs): + + return self.__mul__(*args, **kwargs) + + def mul(self, scalar: Number): """ In-place multiplication with scalar @@ -143,10 +168,10 @@ def mul(self, scalar: PyNumber): self._x *= scalar self._y *= scalar - assert type(self._x) == type(self._y) - self._dtype = type(self._x) - def __matmul__(self, other: Vector2DABC) -> PyNumber: + assert type(self._x) == type(self._y) # very unlikely + + def __matmul__(self, other: Any) -> Union[Number, NotImplementedType]: """ Scalar product between vectors @@ -154,9 +179,12 @@ def __matmul__(self, other: Vector2DABC) -> PyNumber: other : Another vector """ + if not isinstance(other, Vector2DABC): + return NotImplemented + return self.x * other.x + self.y * other.y - def as_dtype(self, dtype: Type) -> Vector2DABC: + def as_dtype(self, dtype: NumberType) -> Vector2DABC: """ Generates new vector with desired data type and returns it. @@ -164,7 +192,7 @@ def as_dtype(self, dtype: Type) -> Vector2DABC: dtype : Desired data type of new vector """ - if dtype == self._dtype: + if dtype == self.dtype: return self.copy() return type(self)(dtype(self._x), dtype(self._y), dtype) @@ -187,7 +215,7 @@ def as_polar_tuple(self) -> Tuple[float, float]: return self.mag, self.angle - def as_tuple(self) -> PyNumber2D: + def as_tuple(self) -> Number2D: """ Exports vector as a tuple """ @@ -196,12 +224,12 @@ def as_tuple(self) -> PyNumber2D: def copy(self) -> Vector2DABC: """ - Copies vector + Copies vector & meta data """ - return type(self)(self._x, self._y, self._dtype) + return type(self)(x = self._x, y = self._y, dtype = self.dtype, meta = self._meta.copy()) - def update(self, x: PyNumber, y: PyNumber): + def update(self, x: Number, y: Number): """ Updates vector components @@ -210,9 +238,10 @@ def update(self, x: PyNumber, y: PyNumber): y : y component. Must have the same type like ``x``. """ - assert type(x) == type(y) + if type(x) != type(y): + raise TypeError('inconsistent dtype') + self._x, self._y = x, y - self._dtype = type(self._x) def update_from_vector(self, other: Vector2DABC): """ @@ -222,9 +251,7 @@ def update_from_vector(self, other: Vector2DABC): other : Another vector. Remains unchanged. """ - assert type(other.x) == type(other.y) self._x, self._y = other.x, other.y - self._dtype = type(self._x) @property def mag(self) -> float: @@ -243,7 +270,7 @@ def angle(self) -> float: return math.atan2(self._y, self._x) @property - def x(self) -> PyNumber: + def x(self) -> Number: """ x component """ @@ -251,16 +278,18 @@ def x(self) -> PyNumber: return self._x @x.setter - def x(self, value: PyNumber): + def x(self, value: Number): """ x component """ - assert isinstance(value, self._dtype) + if not isinstance(value, self.dtype): + raise TypeError('inconsistent dtype') + self._x = value @property - def y(self) -> PyNumber: + def y(self) -> Number: """ y component """ @@ -268,33 +297,45 @@ def y(self) -> PyNumber: return self._y @y.setter - def y(self, value: PyNumber): + def y(self, value: Number): """ y component """ - assert isinstance(value, self._dtype) + if not isinstance(value, self.dtype): + raise TypeError('inconsistent dtype') + self._y = value @property - def dtype(self) -> Type: + def dtype(self) -> NumberType: """ (Python) data type of vector components """ - return self._dtype + return type(self._x) + + @property + def ndim(self) -> int: + """ + Number of dimensions + """ + + return 2 @classmethod - def from_polar(cls, radius: PyNumber, angle: PyNumber) -> Vector2DABC: + def from_polar(cls, radius: Number, angle: Number, meta: Union[MetaDict, None] = None) -> Vector2DABC: """ Generates vector object from polar coordinates Args: radius : A radius angle : An angle in radians + meta : A dict holding arbitrary metadata """ return cls( x = radius * math.cos(angle), y = radius * math.sin(angle), + meta = meta, ) diff --git a/src/bewegung/core/vector/single3d.py b/src/bewegung/linalg/_single3d.py similarity index 60% rename from src/bewegung/core/vector/single3d.py rename to src/bewegung/linalg/_single3d.py index f05de9b..655df5a 100644 --- a/src/bewegung/core/vector/single3d.py +++ b/src/bewegung/linalg/_single3d.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/vector/single3d.py: Single 3D Vector + src/bewegung/linalg/_single3d.py: Single 3D Vector Copyright (C) 2020-2021 Sebastian M. Ernst @@ -29,24 +29,29 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import math -from typing import Tuple, Type, Union - -try: - import numpy as np - from numpy import ndarray -except ModuleNotFoundError: - np, ndarray = None, None -from typeguard import typechecked - -from ..abc import Dtype, PyNumber, PyNumber3D, Vector3DABC, VectorArray3DABC -from ..const import FLOAT_DEFAULT +from numbers import Number +from typing import Any, Tuple, Union + +from ..lib import typechecked +from ._abc import ( + Dtype, + NotImplementedType, + MetaDict, + Number3D, + NumberType, + Vector3DABC, +) +from ._const import FLOAT_DEFAULT +from ._lib import dtype_name +from ._numpy import np, ndarray +from ._single import Vector # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @typechecked -class Vector3D(Vector3DABC): +class Vector3D(Vector, Vector3DABC): """ A single vector in 3D space. @@ -57,31 +62,34 @@ class Vector3D(Vector3DABC): y : y component. Must have the same type like ``x`` and ``z``. z : z component. Must have the same type like ``x`` and ``y``. dtype : Data type. Derived from ``x``, ``y`` and ``z`` if not explicitly provided. + meta : A dict holding arbitrary metadata. """ - _rad2deg = math.pi / 180.0 + _deg2rad = math.pi / 180.0 + _rad2deg = 1.0 / _deg2rad _halfpi = math.pi / 2.0 - def __init__(self, x: PyNumber, y: PyNumber, z: PyNumber, dtype: Union[Type, None] = None): + def __init__(self, x: Number, y: Number, z: Number, dtype: Union[NumberType, None] = None, meta: Union[MetaDict, None] = None): - assert type(x) == type(y) == type(z) if dtype is None: - dtype = type(x) + if not type(x) == type(y) == type(z): + raise TypeError('can not guess dtype - inconsistent') else: - assert dtype == type(x) + x, y, z = dtype(x), dtype(y), dtype(z) - self._x, self._y, self._z, self._dtype = x, y, z, dtype + self._x, self._y, self._z = x, y, z + super().__init__(meta = meta) def __repr__(self) -> str: """ String representation for interactive use """ - if self._dtype == int: - return f'' - return f'' + if self.dtype == int: + return f'' + return f'' - def __eq__(self, other: Vector3DABC) -> bool: + def __eq__(self, other: Any) -> Union[bool, NotImplementedType]: """ Equality check between vectors @@ -89,9 +97,12 @@ def __eq__(self, other: Vector3DABC) -> bool: other : Another vector """ - return (self.x == other.x) and (self.y == other.y) and (self.z == other.z) + if not isinstance(other, Vector3DABC): + return NotImplemented - def __mod__(self, other: Vector3DABC) -> bool: + return bool(self.x == other.x) and bool(self.y == other.y) and bool(self.z == other.z) + + def __mod__(self, other: Any) -> Union[bool, NotImplementedType]: """ Is-close check (relevant for dtype ``float``) between vectors @@ -99,9 +110,12 @@ def __mod__(self, other: Vector3DABC) -> bool: other : Another vector """ + if not isinstance(other, Vector3DABC): + return NotImplemented + return math.isclose(self.x, other.x) and math.isclose(self.y, other.y) and math.isclose(self.z, other.z) - def __add__(self, other: Union[Vector3DABC, VectorArray3DABC]) -> Vector3DABC: + def __add__(self, other: Any) -> Union[Vector3DABC, NotImplementedType]: """ Add operation between vectors or a vector and a vector array @@ -109,12 +123,12 @@ def __add__(self, other: Union[Vector3DABC, VectorArray3DABC]) -> Vector3DABC: other : Another vector """ - if isinstance(other, VectorArray3DABC): - return NotImplemented # hand off to array type + if not isinstance(other, Vector3DABC): + return NotImplemented return type(self)(self.x + other.x, self.y + other.y, self.z + other.z) - def __sub__(self, other: Union[Vector3DABC, VectorArray3DABC]) -> Vector3DABC: + def __sub__(self, other: Any) -> Union[Vector3DABC, NotImplementedType]: """ Substract operator between vectors or a vector and a vector array @@ -122,12 +136,12 @@ def __sub__(self, other: Union[Vector3DABC, VectorArray3DABC]) -> Vector3DABC: other : Another vector """ - if isinstance(other, VectorArray3DABC): - return NotImplemented # hand off to array type + if not isinstance(other, Vector3DABC): + return NotImplemented return type(self)(self.x - other.x, self.y - other.y, self.z - other.z) - def __mul__(self, other: PyNumber) -> Vector3DABC: + def __mul__(self, other: Any) -> Union[Vector3DABC, NotImplementedType]: """ Multiplication with scalar @@ -135,9 +149,16 @@ def __mul__(self, other: PyNumber) -> Vector3DABC: other : A number """ + if not isinstance(other, Number): + return NotImplemented + return type(self)(self._x * other, self._y * other, self._z * other) - def mul(self, scalar: PyNumber): + def __rmul__(self, *args, **kwargs): + + return self.__mul__(*args, **kwargs) + + def mul(self, scalar: Number): """ In-place multiplication with scalar @@ -148,10 +169,10 @@ def mul(self, scalar: PyNumber): self._x *= scalar self._y *= scalar self._z *= scalar - assert type(self._x) == type(self._y) == type(self._z) - self._dtype = type(self._x) - def __matmul__(self, other: Vector3DABC) -> PyNumber: + assert type(self._x) == type(self._y) == type(self._z) # very unlikely + + def __matmul__(self, other: Any) -> Union[Number, NotImplementedType]: """ Scalar product between vectors @@ -159,9 +180,12 @@ def __matmul__(self, other: Vector3DABC) -> PyNumber: other : Another vector """ + if not isinstance(other, Vector3DABC): + return NotImplemented + return self.x * other.x + self.y * other.y + self.z * other.z - def as_dtype(self, dtype: Type) -> Vector3DABC: + def as_dtype(self, dtype: NumberType) -> Vector3DABC: """ Generates new vector with desired data type and returns it. @@ -169,7 +193,7 @@ def as_dtype(self, dtype: Type) -> Vector3DABC: dtype : Desired data type of new vector """ - if dtype == self._dtype: + if dtype == self.dtype: return self.copy() return type(self)(dtype(self._x), dtype(self._y), dtype(self._z), dtype) @@ -192,7 +216,14 @@ def as_polar_tuple(self) -> Tuple[float, float, float]: return (self.mag, self.theta, self.phi) - def as_tuple(self) -> PyNumber3D: + def as_geographic_tuple(self) -> Tuple[float, float, float]: + """ + Exports vector as a tuple of geographic coordinates (radius, lon, lat) + """ + + return (self.mag, self.lon, self.lat) + + def as_tuple(self) -> Number3D: """ Exports vector as a tuple """ @@ -201,12 +232,12 @@ def as_tuple(self) -> PyNumber3D: def copy(self) -> Vector3DABC: """ - Copies vector + Copies vector & meta data """ - return type(self)(self._x, self._y, self._z, self._dtype) + return type(self)(x = self._x, y = self._y, z = self._z, dtype = self.dtype, meta = self._meta.copy()) - def update(self, x: PyNumber, y: PyNumber, z: PyNumber): + def update(self, x: Number, y: Number, z: Number): """ Updates vector components @@ -216,9 +247,10 @@ def update(self, x: PyNumber, y: PyNumber, z: PyNumber): z : z component. Must have the same type like ``x`` and ``y``. """ - assert type(x) == type(y) == type(z) + if not type(x) == type(y) == type(z): + raise TypeError('inconsistent dtype') + self._x, self._y, self._z = x, y, z - self._dtype = type(self._x) def update_from_vector(self, other: Vector3DABC): """ @@ -228,9 +260,7 @@ def update_from_vector(self, other: Vector3DABC): other : Another vector. Remains unchanged. """ - assert type(other.x) == type(other.y) == type(other.z) self._x, self._y, self._z = other.x, other.y, other.z - self._dtype = type(self._x) @property def mag(self) -> float: @@ -257,7 +287,23 @@ def phi(self) -> float: return math.atan2(self._y, self._x) @property - def x(self) -> PyNumber: + def lat(self) -> float: + """ + The vector's geographic latitude in degree, computed on demand + """ + + return -(self.theta - self._halfpi) * self._rad2deg + + @property + def lon(self) -> float: + """ + The vector's gepgraphic longitude in degree, computed on demand + """ + + return self.phi * self._rad2deg + + @property + def x(self) -> Number: """ x component """ @@ -265,16 +311,18 @@ def x(self) -> PyNumber: return self._x @x.setter - def x(self, value: PyNumber): + def x(self, value: Number): """ x component """ - assert isinstance(value, self._dtype) + if not isinstance(value, self.dtype): + raise TypeError('inconsistent dtype') + self._x = value @property - def y(self) -> PyNumber: + def y(self) -> Number: """ y component """ @@ -282,16 +330,18 @@ def y(self) -> PyNumber: return self._y @y.setter - def y(self, value: PyNumber): + def y(self, value: Number): """ y component """ - assert isinstance(value, self._dtype) + if not isinstance(value, self.dtype): + raise TypeError('inconsistent dtype') + self._y = value @property - def z(self) -> PyNumber: + def z(self) -> Number: """ z component """ @@ -299,24 +349,34 @@ def z(self) -> PyNumber: return self._z @z.setter - def z(self, value: PyNumber): + def z(self, value: Number): """ z component """ - assert isinstance(value, self._dtype) + if not isinstance(value, self.dtype): + raise TypeError('inconsistent dtype') + self._z = value @property - def dtype(self) -> Type: + def dtype(self) -> NumberType: """ (Python) data type of vector components """ - return self._dtype + return type(self._x) + + @property + def ndim(self) -> int: + """ + Number of dimensions + """ + + return 3 @classmethod - def from_polar(cls, radius: PyNumber, theta: PyNumber, phi: PyNumber) -> Vector3DABC: + def from_polar(cls, radius: Number, theta: Number, phi: Number, meta: Union[MetaDict, None] = None) -> Vector3DABC: """ Generates vector object from polar coordinates @@ -324,6 +384,7 @@ def from_polar(cls, radius: PyNumber, theta: PyNumber, phi: PyNumber) -> Vector3 radius : A radius theta : An angle in radians phi : An angle in radians + meta : A dict holding arbitrary metadata """ RadiusSinTheta = radius * math.sin(theta) @@ -331,10 +392,11 @@ def from_polar(cls, radius: PyNumber, theta: PyNumber, phi: PyNumber) -> Vector3 x = RadiusSinTheta * math.cos(phi), y = RadiusSinTheta * math.sin(phi), z = radius * math.cos(theta), + meta = meta, ) @classmethod - def from_geographic(cls, radius: PyNumber, lon: PyNumber, lat: PyNumber) -> Vector3DABC: + def from_geographic(cls, radius: Number, lon: Number, lat: Number, meta: Union[MetaDict, None] = None) -> Vector3DABC: """ Generates vector object from geographic polar coordinates @@ -342,10 +404,12 @@ def from_geographic(cls, radius: PyNumber, lon: PyNumber, lat: PyNumber) -> Vect radius : A radius lon : An angle in degree lat : An angle in degree + meta : A dict holding arbitrary metadata """ return cls.from_polar( radius = radius, - theta = cls._halfpi - (lat * cls._rad2deg), - phi = lon * cls._rad2deg, + theta = cls._halfpi - (lat * cls._deg2rad), + phi = lon * cls._deg2rad, + meta = meta, ) diff --git a/src/bewegung/linalg/_svg.py b/src/bewegung/linalg/_svg.py new file mode 100644 index 0000000..e0b4234 --- /dev/null +++ b/src/bewegung/linalg/_svg.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + src/bewegung/linalg/_svg.py: SVG output for vectors + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from math import ceil, floor, log10, pi +from numbers import Number +from typing import Union + +from ..lib import Color, typechecked +from ._array import VectorArray +from ._single import Vector + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class Svg: + """ + Wrap vectors into SVG + """ + + def __init__(self, vec: Union[Vector, VectorArray], size: Number = 300): + + size = float(size) + assert size > 0 + self._size = size + + self._vectors = [] + self._radius = 0 + self._view = 0 + self._scale_factor = 0 + self._step = 0 + + if isinstance(vec, Vector): + self._add_vector(vec) + else: + self._add_vectors(vec) + + def _add_vector(self, vector: Vector): + + self._update(vector.x) + self._update(vector.y) + + self._vectors.append(vector) + + def _add_vectors(self, vectors: VectorArray): + + for vector in vectors: + self._add_vector(vector) + + def _update(self, value: Number): + + value = abs(float(value)) + + if value <= self._radius: + return + + self._radius = value + + self._scale_factor = 2 * self._radius / self._size + + pos = floor(log10(self._radius)) + self._view = ceil(self._radius / (10 ** pos)) * (10 ** pos) + + self._step = 10 ** floor(log10(self._radius)) + + def _line( + self, + x1: float = 0.0, y1: float = 0.0, + x2: float = 0.0, y2: float = 0.0, + color: str = '#FF0000', + opacity: float = 1.0, + width: float = 1.0, + dashed: bool = False, + m1: bool = False, + m2: bool = False, + ): + + assert width > 0 + assert len(color) == 7 and color[0] == '#' + assert 0.0 <= opacity <= 1.0 + + if (x1, y1) == (x2, y2): + return '' + + dashes = f'stroke-dasharray="{self._scale_factor*4:e} {self._scale_factor*1:e}" ' if dashed else '' + markerid = f'arrow_{hash((x1, y1, x2, y2)):x}' if m1 or m2 else '' + markerfactor = self._size / 50 + marker = ( + '' + f'' + f'' + '' + '' + ) if m1 or m2 else '' + markerstart = f'marker-start="url(#{markerid:s})" ' if m1 else '' + markerend = f'marker-end="url(#{markerid:s})" ' if m2 else '' + + return ( + f'{marker:s}' + '' + ) + + def _grid(self): + + lines = [ + self._line(x1 = -self._view, x2 = self._view, color = '#808080'), + self._line(y1 = -self._view, y2 = self._view, color = '#808080'), + ] + + for idx in range(-10, 11): + if idx == 0: + continue + tock = idx % 10 == 0 + val = float(idx * self._step) + lines.append(self._line( + x1 = -self._view, y1 = val, + x2 = self._view, y2 = val, + color = '#808080' if tock else '#C0C0C0', + dashed = True, + )) + lines.append(self._line( + x1 = val, y1 = -self._view, + x2 = val, y2 = self._view, + color = '#808080' if tock else '#C0C0C0', + dashed = True, + )) + + return ''.join(lines) + + def _vector(self, vector: Vector) -> str: + + color = Color.from_hsv(vector.angle * 180 / pi, 1.0, 1.0).as_hex(alpha = False) + + return self._line(x2 = vector.x, y2 = vector.y, color = f'#{color:s}', m2 = True) + + def render(self) -> str: + + assert len(self._vectors) > 0 + assert self._radius > 0 + assert self._view >= self._radius + assert self._scale_factor > 0 + + return ( + '' + + f'' # invert y axis + f'{self._grid():s}' + f'{"".join([self._vector(vec) for vec in self._vectors]):s}' + '' + + '' + f'' + f'tick={self._step:0.0e}' + '' + + '' + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..90d3596 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + tests/__init__.py: Test package root + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import os as _os + +from hypothesis import settings as _settings + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# SETTINGS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +_settings.register_profile("dev", max_examples = 300) +_settings.load_profile(_os.getenv("HYPOTHESIS_PROFILE", "default")) diff --git a/src/bewegung/core/__init__.py b/tests/linalg/__init__.py similarity index 93% rename from src/bewegung/core/__init__.py rename to tests/linalg/__init__.py index 87c3336..9957eff 100644 --- a/src/bewegung/core/__init__.py +++ b/tests/linalg/__init__.py @@ -6,7 +6,7 @@ a versatile video renderer https://github.com/pleiszenburg/bewegung - src/bewegung/core/__init__.py: Package core root + tests/linalg/__init__.py: Linear algebra tests Copyright (C) 2020-2021 Sebastian M. Ernst diff --git a/tests/linalg/test_single2d_checks.py b/tests/linalg/test_single2d_checks.py new file mode 100644 index 0000000..af7c89c --- /dev/null +++ b/tests/linalg/test_single2d_checks.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + tests/linalg/test_single2d_checks.py: Vector 2D checks + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from math import isnan, isclose, pi, sqrt + +import numpy as np +from hypothesis import ( + given, + strategies as st, +) +import pytest + +from bewegung import Vector2D + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: INT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_repr(): + + v1 = Vector2D(0, 0) + v2 = Vector2D(0.0, 0.0) + + v1r = repr(v1) + v2r = repr(v2) + + assert 'Vector2D' in v1r + assert 'Vector2D' in v2r + + assert 'dtype=int' in v1r + assert 'dtype=float' in v2r + +@given( + x1 = st.floats(), + y1 = st.floats(), +) +def test_eq(x1, y1): + + v1 = Vector2D(x1, y1) + + if not isnan(x1) and not isnan(y1): + assert v1 == v1 + assert v1 % v1 + else: + assert v1 != v1 + assert not v1 % v1 + + v1c = v1.copy() + assert v1 is not v1c + + if not isnan(x1) and not isnan(y1): + assert v1 == v1c + assert v1 % v1c + else: + assert v1 != v1c + assert not v1 % v1c + +@given( + x1 = st.integers(), + y1 = st.integers(), +) +def test_eq_types(x1, y1): + + x1f = float(x1) + y1f = float(y1) + + v1 = Vector2D(x1, y1) + v1f = Vector2D(x1f, y1f) + + assert v1 % v1f + + if x1f == x1 and y1f == y1: + assert v1f == v1 + else: + assert v1f != v1 + +def test_dtype_basic(): + + v1 = Vector2D(0, 0) + v2 = Vector2D(0.0, 0.0) + + assert v1 == v2 + assert v1 % v2 + + v1i = v1.as_dtype(int) + v1f = v1.as_dtype(float) + + assert v1i == v1 + assert v1i is not v1 + assert v1i.dtype == int + + assert v1f == v1 + assert v1f is not v1 + assert v1f.dtype == float + + v2i = v2.as_dtype(int) + v2f = v2.as_dtype(float) + + assert v2i == v2 + assert v2i is not v2 + assert v2i.dtype == int + + assert v2f == v2 + assert v2f is not v2 + assert v2f.dtype == float + +def test_dtype_np(): + + v1 = Vector2D(np.int8(0), np.int8(0)) + v2 = Vector2D(np.float32(0.0), np.float32(0.0)) + + assert v1 == v2 + assert v1 % v2 + + assert v1.dtype == np.int8 + assert v2.dtype == np.float32 + + v3 = v1.as_dtype(int) + assert v3.dtype == int + + v4 = v2.as_dtype(float) + assert v4.dtype == float + + v5 = v1 + v2 + assert v5.dtype == np.float32 + +def test_dtype_error(): + + with pytest.raises(TypeError): + _ = Vector2D(0, 0.0) + + assert Vector2D(0, 0.0, int).dtype == int + assert Vector2D(0, 0.0, float).dtype == float + + v1 = Vector2D(0, 0, int) + v1.update(1.0, 1.0) + assert v1.dtype == float + with pytest.raises(TypeError): + v1.update(2, 2.0) + + assert isinstance(v1.x, float) + assert isinstance(v1.y, float) + + with pytest.raises(TypeError): + v1.x = 4 + with pytest.raises(TypeError): + v1.y = 4 + + v1.x = 5.0 + assert v1 == Vector2D(5.0, 1.0) + v1.y = 6.0 + assert v1 == Vector2D(5.0, 6.0) + +def test_ndim(): + + v1 = Vector2D(0, 0) + + assert v1.ndim == 2 + +def test_extra(): + + v1 = Vector2D(3, 4) + assert isinstance(v1.mag, float) + assert isclose(v1.mag, 5.0) + + v2 = Vector2D(1, 1) + assert isinstance(v2.angle, float) + assert isclose(v2.angle, pi / 4) + + mag, angle = v2.as_polar_tuple() + assert isinstance(mag, float) + assert isclose(mag, sqrt(2)) + assert isinstance(angle, float) + assert isclose(angle, pi / 4) + + assert v2 % Vector2D.from_polar(sqrt(2), pi / 4) + +def test_update(): + + v1 = Vector2D(0, 0) + + v1.update(1, 1) + assert v1.dtype == int + assert v1 == Vector2D(1, 1) + + v1.update(2.0, 2.0) + assert v1.dtype == float + assert v1 == Vector2D(2.0, 2.0) + + v2 = Vector2D(3, 3) + v1.update_from_vector(v2) + assert v1 is not v2 + assert v1 == v2 + assert v1.dtype == int + assert v1 == Vector2D(3, 3) + + v3 = Vector2D(4.0, 4.0) + v1.update_from_vector(v3) + assert v1 is not v3 + assert v1 == v3 + assert v1.dtype == float + assert v1 == Vector2D(4.0, 4.0) + +def test_tuple(): + + v1 = Vector2D(0, 0) + v2 = Vector2D(0.0, 0.0) + + v1t = v1.as_tuple() + v2t = v2.as_tuple() + + assert len(v1t) == 2 + assert len(v2t) == 2 + + assert all(isinstance(item, int) for item in v1t) + assert all(isinstance(item, float) for item in v2t) + + assert v1t == (0, 0) + assert v2t == (0.0, 0.0) diff --git a/tests/linalg/test_single2d_operations.py b/tests/linalg/test_single2d_operations.py new file mode 100644 index 0000000..fc6b0e4 --- /dev/null +++ b/tests/linalg/test_single2d_operations.py @@ -0,0 +1,305 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + tests/linalg/test_single2d_operations.py: Vector operations 2D + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from math import isnan + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +from bewegung import Vector2D + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: INT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@given( + x1 = st.integers(), + y1 = st.integers(), + x2 = st.integers(), + y2 = st.integers(), +) +def test_add_int(x1, y1, x2, y2): + + v1 = Vector2D(x1, y1) + v2 = Vector2D(x2, y2) + + v3 = v1 + v2 + + assert v3.x == x1 + x2 + assert v3.y == y1 + y2 + +@given( + x1 = st.integers(), + y1 = st.integers(), + x2 = st.integers(), + y2 = st.integers(), +) +def test_sub_int(x1, y1, x2, y2): + + v1 = Vector2D(x1, y1) + v2 = Vector2D(x2, y2) + + v3 = v1 - v2 + + assert v3.x == x1 - x2 + assert v3.y == y1 - y2 + +@given( + x1 = st.integers(), + y1 = st.integers(), + scalar = st.floats() | st.integers(), +) +def test_mul_int(x1, y1, scalar): + + v1 = Vector2D(x1, y1) + + v2 = v1 * scalar + + x2 = x1 * scalar + y2 = y1 * scalar + + assert type(scalar) == v2.dtype + + if isnan(x2): + assert isnan(v2.x) + else: + assert v2.x == x2 + + if isnan(y2): + assert isnan(v2.y) + else: + assert v2.y == y2 + +@given( + x1 = st.integers(), + y1 = st.integers(), + scalar = st.floats() | st.integers(), +) +def test_mul_inplace_int(x1, y1, scalar): + + v1 = Vector2D(x1, y1) + + v1.mul(scalar) + + x_ = x1 * scalar + y_ = y1 * scalar + + assert type(scalar) == v1.dtype + + if isnan(x_): + assert isnan(v1.x) + else: + assert v1.x == x_ + + if isnan(y_): + assert isnan(v1.y) + else: + assert v1.y == y_ + +def test_matmul_int(): + + v1 = Vector2D(2, 3) + v2 = Vector2D(5, 7) + + s = v1 @ v2 + + assert isinstance(s, int) + assert s == 31 + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: FLOAT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@given( + x1 = st.floats(), + y1 = st.floats(), + x2 = st.floats(), + y2 = st.floats(), +) +def test_add_float(x1, y1, x2, y2): + + v1 = Vector2D(x1, y1) + v2 = Vector2D(x2, y2) + + v3 = v1 + v2 + + x3 = x1 + x2 + y3 = y1 + y2 + + if isnan(x3): + assert isnan(v3.x) + else: + assert v3.x == x3 + + if isnan(y3): + assert isnan(v3.y) + else: + assert v3.y == y3 + +@given( + x1 = st.floats(), + y1 = st.floats(), + x2 = st.floats(), + y2 = st.floats(), +) +def test_sub_float(x1, y1, x2, y2): + + v1 = Vector2D(x1, y1) + v2 = Vector2D(x2, y2) + + v3 = v1 - v2 + + x3 = x1 - x2 + y3 = y1 - y2 + + if isnan(x3): + assert isnan(v3.x) + else: + assert v3.x == x3 + + if isnan(y3): + assert isnan(v3.y) + else: + assert v3.y == y3 + +@given( + x1 = st.floats(), + y1 = st.floats(), + scalar = st.floats() | st.integers(), +) +def test_mul_float(x1, y1, scalar): + + v1 = Vector2D(x1, y1) + + v2 = v1 * scalar + + x2 = x1 * scalar + y2 = y1 * scalar + + assert float == v2.dtype + + if isnan(x2): + assert isnan(v2.x) + else: + assert v2.x == x2 + + if isnan(y2): + assert isnan(v2.y) + else: + assert v2.y == y2 + +@given( + x1 = st.floats(), + y1 = st.floats(), + scalar = st.floats() | st.integers(), +) +def test_mul_inplace_float(x1, y1, scalar): + + v1 = Vector2D(x1, y1) + + v1.mul(scalar) + + x_ = x1 * scalar + y_ = y1 * scalar + + assert float == v1.dtype + + if isnan(x_): + assert isnan(v1.x) + else: + assert v1.x == x_ + + if isnan(y_): + assert isnan(v1.y) + else: + assert v1.y == y_ + +def test_matmul_float(): + + v1 = Vector2D(2.0, 3.0) + v2 = Vector2D(5.0, 7.0) + + s = v1 @ v2 + + assert isinstance(s, float) + assert s == 31.0 + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: R-OPERATIONS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_rmul(): + + v1 = Vector2D(2, 3) + + v2 = 4 * v1 + + assert isinstance(v2, Vector2D) + assert v1 is not v2 + assert v2 == Vector2D(8, 12) + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: MISC +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_notimplemented(): + + v1 = Vector2D(2, 3) + + assert v1 != 1 + assert 1 != v1 + + with pytest.raises(TypeError): + _ = v1 % 1 + with pytest.raises(TypeError): + _ = 1 % v1 + + with pytest.raises(TypeError): + _ = v1 + 1 + with pytest.raises(TypeError): + _ = 1 + v1 + + with pytest.raises(TypeError): + _ = v1 - 1 + with pytest.raises(TypeError): + _ = 1 - v1 + + with pytest.raises(TypeError): + _ = v1 * "1" + with pytest.raises(TypeError): + _ = "1" * v1 + + with pytest.raises(TypeError): + _ = v1 @ 1 + with pytest.raises(TypeError): + _ = 1 @ v1 diff --git a/tests/linalg/test_single3d_checks.py b/tests/linalg/test_single3d_checks.py new file mode 100644 index 0000000..b0f835c --- /dev/null +++ b/tests/linalg/test_single3d_checks.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + tests/linalg/test_single2d_checks.py: Vector 3D checks + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from math import isnan, isclose, pi, sqrt + +import numpy as np +from hypothesis import ( + given, + strategies as st, +) +import pytest + +from bewegung import Vector3D + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: INT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_repr(): + + v1 = Vector3D(0, 0, 0) + v2 = Vector3D(0.0, 0.0, 0.0) + + v1r = repr(v1) + v2r = repr(v2) + + assert 'Vector3D' in v1r + assert 'Vector3D' in v2r + + assert 'dtype=int' in v1r + assert 'dtype=float' in v2r + +@given( + x1 = st.floats(), + y1 = st.floats(), + z1 = st.floats(), +) +def test_eq(x1, y1, z1): + + v1 = Vector3D(x1, y1, z1) + + if not isnan(x1) and not isnan(y1) and not isnan(z1): + assert v1 == v1 + assert v1 % v1 + else: + assert v1 != v1 + assert not v1 % v1 + + v1c = v1.copy() + assert v1 is not v1c + + if not isnan(x1) and not isnan(y1) and not isnan(z1): + assert v1 == v1c + assert v1 % v1c + else: + assert v1 != v1c + assert not v1 % v1c + +@given( + x1 = st.integers(), + y1 = st.integers(), + z1 = st.integers(), +) +def test_eq_types(x1, y1, z1): + + x1f = float(x1) + y1f = float(y1) + z1f = float(z1) + + v1 = Vector3D(x1, y1, z1) + v1f = Vector3D(x1f, y1f, z1f) + + assert v1 % v1f + + if x1f == x1 and y1f == y1 and z1f == z1: + assert v1f == v1 + else: + assert v1f != v1 + +def test_dtype_basic(): + + v1 = Vector3D(0, 0, 0) + v2 = Vector3D(0.0, 0.0, 0.0) + + assert v1 == v2 + assert v1 % v2 + + v1i = v1.as_dtype(int) + v1f = v1.as_dtype(float) + + assert v1i == v1 + assert v1i is not v1 + assert v1i.dtype == int + + assert v1f == v1 + assert v1f is not v1 + assert v1f.dtype == float + + v2i = v2.as_dtype(int) + v2f = v2.as_dtype(float) + + assert v2i == v2 + assert v2i is not v2 + assert v2i.dtype == int + + assert v2f == v2 + assert v2f is not v2 + assert v2f.dtype == float + +def test_dtype_np(): + + v1 = Vector3D(np.int8(0), np.int8(0), np.int8(0)) + v2 = Vector3D(np.float32(0.0), np.float32(0.0), np.float32(0.0)) + + assert v1 == v2 + assert v1 % v2 + + assert v1.dtype == np.int8 + assert v2.dtype == np.float32 + + v3 = v1.as_dtype(int) + assert v3.dtype == int + + v4 = v2.as_dtype(float) + assert v4.dtype == float + + v5 = v1 + v2 + assert v5.dtype == np.float32 + +def test_dtype_error(): + + with pytest.raises(TypeError): + _ = Vector3D(0, 0.0, 0.0) + + assert Vector3D(0, 0.0, 0.0, int).dtype == int + assert Vector3D(0, 0.0, 0.0, float).dtype == float + + v1 = Vector3D(0, 0, 0, int) + v1.update(1.0, 1.0, 1.0) + assert v1.dtype == float + with pytest.raises(TypeError): + v1.update(2, 2.0, 2.0) + + assert isinstance(v1.x, float) + assert isinstance(v1.y, float) + assert isinstance(v1.z, float) + + with pytest.raises(TypeError): + v1.x = 4 + with pytest.raises(TypeError): + v1.y = 4 + with pytest.raises(TypeError): + v1.z = 4 + + v1.x = 5.0 + assert v1 == Vector3D(5.0, 1.0, 1.0) + v1.y = 6.0 + assert v1 == Vector3D(5.0, 6.0, 1.0) + v1.z = 7.0 + assert v1 == Vector3D(5.0, 6.0, 7.0) + +def test_ndim(): + + v1 = Vector3D(0, 0, 0) + + assert v1.ndim == 3 + +def test_extra(): + + v1 = Vector3D(2, 3, 6) + assert isinstance(v1.mag, float) + assert isclose(v1.mag, 7.0) + + v2 = Vector3D(1, 1, 1) + assert isinstance(v2.theta, float) + assert isinstance(v2.phi, float) + assert isclose(v2.theta, 0.9553166181245092) + assert isclose(v2.phi, 0.7853981633974483) + assert isinstance(v2.lon, float) + assert isinstance(v2.lat, float) + assert isclose(v2.lon, 45.0) + assert isclose(v2.lat, 35.264389682754654) + + mag, theta, phi = v2.as_polar_tuple() + assert isinstance(mag, float) + assert isclose(mag, sqrt(3)) + assert isinstance(theta, float) + assert isclose(theta, 0.9553166181245092) + assert isinstance(phi, float) + assert isclose(phi, 0.7853981633974483) + + mag, lon, lat = v2.as_geographic_tuple() + assert isinstance(mag, float) + assert isclose(mag, sqrt(3)) + assert isinstance(lon, float) + assert isclose(lon, 45.0) + assert isinstance(lat, float) + assert isclose(lat, 35.264389682754654) + + assert v2 % Vector3D.from_polar(sqrt(3), 0.9553166181245092, 0.7853981633974483) + assert v2 % Vector3D.from_geographic(sqrt(3), 45.0, 35.264389682754654) + +def test_update(): + + v1 = Vector3D(0, 0, 0) + + v1.update(1, 1, 1) + assert v1.dtype == int + assert v1 == Vector3D(1, 1, 1) + + v1.update(2.0, 2.0, 2.0) + assert v1.dtype == float + assert v1 == Vector3D(2.0, 2.0, 2.0) + + v2 = Vector3D(3, 3, 3) + v1.update_from_vector(v2) + assert v1 is not v2 + assert v1 == v2 + assert v1.dtype == int + assert v1 == Vector3D(3, 3, 3) + + v3 = Vector3D(4.0, 4.0, 4.0) + v1.update_from_vector(v3) + assert v1 is not v3 + assert v1 == v3 + assert v1.dtype == float + assert v1 == Vector3D(4.0, 4.0, 4.0) + +def test_tuple(): + + v1 = Vector3D(0, 0, 0) + v2 = Vector3D(0.0, 0.0, 0.0) + + v1t = v1.as_tuple() + v2t = v2.as_tuple() + + assert len(v1t) == 3 + assert len(v2t) == 3 + + assert all(isinstance(item, int) for item in v1t) + assert all(isinstance(item, float) for item in v2t) + + assert v1t == (0, 0, 0) + assert v2t == (0.0, 0.0, 0.0) diff --git a/tests/linalg/test_single3d_operations.py b/tests/linalg/test_single3d_operations.py new file mode 100644 index 0000000..1deb378 --- /dev/null +++ b/tests/linalg/test_single3d_operations.py @@ -0,0 +1,355 @@ +# -*- coding: utf-8 -*- + +""" + +BEWEGUNG +a versatile video renderer +https://github.com/pleiszenburg/bewegung + + tests/linalg/test_single2d_operations.py: Vector operations 3D + + Copyright (C) 2020-2021 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/bewegung/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from math import isnan + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +from bewegung import Vector3D + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: INT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@given( + x1 = st.integers(), + y1 = st.integers(), + z1 = st.integers(), + x2 = st.integers(), + y2 = st.integers(), + z2 = st.integers(), +) +def test_add_int(x1, y1, z1, x2, y2, z2): + + v1 = Vector3D(x1, y1, z1) + v2 = Vector3D(x2, y2, z2) + + v3 = v1 + v2 + + assert v3.x == x1 + x2 + assert v3.y == y1 + y2 + assert v3.z == z1 + z2 + +@given( + x1 = st.integers(), + y1 = st.integers(), + z1 = st.integers(), + x2 = st.integers(), + y2 = st.integers(), + z2 = st.integers(), +) +def test_sub_int(x1, y1, z1, x2, y2, z2): + + v1 = Vector3D(x1, y1, z1) + v2 = Vector3D(x2, y2, z2) + + v3 = v1 - v2 + + assert v3.x == x1 - x2 + assert v3.y == y1 - y2 + assert v3.z == z1 - z2 + +@given( + x1 = st.integers(), + y1 = st.integers(), + z1 = st.integers(), + scalar = st.floats() | st.integers(), +) +def test_mul_int(x1, y1, z1, scalar): + + v1 = Vector3D(x1, y1, z1) + + v2 = v1 * scalar + + x2 = x1 * scalar + y2 = y1 * scalar + z2 = z1 * scalar + + assert type(scalar) == v2.dtype + + if isnan(x2): + assert isnan(v2.x) + else: + assert v2.x == x2 + + if isnan(y2): + assert isnan(v2.y) + else: + assert v2.y == y2 + + if isnan(z2): + assert isnan(v2.z) + else: + assert v2.z == z2 + +@given( + x1 = st.integers(), + y1 = st.integers(), + z1 = st.integers(), + scalar = st.floats() | st.integers(), +) +def test_mul_inplace_int(x1, y1, z1, scalar): + + v1 = Vector3D(x1, y1, z1) + + v1.mul(scalar) + + x_ = x1 * scalar + y_ = y1 * scalar + z_ = z1 * scalar + + assert type(scalar) == v1.dtype + + if isnan(x_): + assert isnan(v1.x) + else: + assert v1.x == x_ + + if isnan(y_): + assert isnan(v1.y) + else: + assert v1.y == y_ + + if isnan(z_): + assert isnan(v1.z) + else: + assert v1.z == z_ + +def test_matmul_int(): + + v1 = Vector3D(2, 3, 4) + v2 = Vector3D(5, 7, 8) + + s = v1 @ v2 + + assert isinstance(s, int) + assert s == 63 + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: FLOAT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@given( + x1 = st.floats(), + y1 = st.floats(), + z1 = st.floats(), + x2 = st.floats(), + y2 = st.floats(), + z2 = st.floats(), +) +def test_add_float(x1, y1, z1, x2, y2, z2): + + v1 = Vector3D(x1, y1, z1) + v2 = Vector3D(x2, y2, z2) + + v3 = v1 + v2 + + x3 = x1 + x2 + y3 = y1 + y2 + z3 = z1 + z2 + + if isnan(x3): + assert isnan(v3.x) + else: + assert v3.x == x3 + + if isnan(y3): + assert isnan(v3.y) + else: + assert v3.y == y3 + + if isnan(z3): + assert isnan(v3.z) + else: + assert v3.z == z3 + +@given( + x1 = st.floats(), + y1 = st.floats(), + z1 = st.floats(), + x2 = st.floats(), + y2 = st.floats(), + z2 = st.floats(), +) +def test_sub_float(x1, y1, z1, x2, y2, z2): + + v1 = Vector3D(x1, y1, z1) + v2 = Vector3D(x2, y2, z2) + + v3 = v1 - v2 + + x3 = x1 - x2 + y3 = y1 - y2 + z3 = z1 - z2 + + if isnan(x3): + assert isnan(v3.x) + else: + assert v3.x == x3 + + if isnan(y3): + assert isnan(v3.y) + else: + assert v3.y == y3 + + if isnan(z3): + assert isnan(v3.z) + else: + assert v3.z == z3 + +@given( + x1 = st.floats(), + y1 = st.floats(), + z1 = st.floats(), + scalar = st.floats() | st.integers(), +) +def test_mul_float(x1, y1, z1, scalar): + + v1 = Vector3D(x1, y1, z1) + + v2 = v1 * scalar + + x2 = x1 * scalar + y2 = y1 * scalar + z2 = z1 * scalar + + assert float == v2.dtype + + if isnan(x2): + assert isnan(v2.x) + else: + assert v2.x == x2 + + if isnan(y2): + assert isnan(v2.y) + else: + assert v2.y == y2 + + if isnan(z2): + assert isnan(v2.z) + else: + assert v2.z == z2 + +@given( + x1 = st.floats(), + y1 = st.floats(), + z1 = st.floats(), + scalar = st.floats() | st.integers(), +) +def test_mul_inplace_float(x1, y1, z1, scalar): + + v1 = Vector3D(x1, y1, z1) + + v1.mul(scalar) + + x_ = x1 * scalar + y_ = y1 * scalar + z_ = z1 * scalar + + assert float == v1.dtype + + if isnan(x_): + assert isnan(v1.x) + else: + assert v1.x == x_ + + if isnan(y_): + assert isnan(v1.y) + else: + assert v1.y == y_ + + if isnan(z_): + assert isnan(v1.z) + else: + assert v1.z == z_ + +def test_matmul_float(): + + v1 = Vector3D(2.0, 3.0, 4.0) + v2 = Vector3D(5.0, 7.0, 8.0) + + s = v1 @ v2 + + assert isinstance(s, float) + assert s == 63.0 + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: R-OPERATIONS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_rmul(): + + v1 = Vector3D(2, 3, 4) + + v2 = 5 * v1 + + assert isinstance(v2, Vector3D) + assert v1 is not v2 + assert v2 == Vector3D(10, 15, 20) + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TESTS: MISC +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_notimplemented(): + + v1 = Vector3D(2, 3, 4) + + assert v1 != 1 + assert 1 != v1 + + with pytest.raises(TypeError): + _ = v1 % 1 + with pytest.raises(TypeError): + _ = 1 % v1 + + with pytest.raises(TypeError): + _ = v1 + 1 + with pytest.raises(TypeError): + _ = 1 + v1 + + with pytest.raises(TypeError): + _ = v1 - 1 + with pytest.raises(TypeError): + _ = 1 - v1 + + with pytest.raises(TypeError): + _ = v1 * "1" + with pytest.raises(TypeError): + _ = "1" * v1 + + with pytest.raises(TypeError): + _ = v1 @ 1 + with pytest.raises(TypeError): + _ = 1 @ v1