├── tests ├── __init__.py ├── boxes │ └── __init__.py ├── main │ └── __init__.py ├── temporal │ ├── __init__.py │ └── interpolation_test.py ├── collections │ ├── __init__.py │ ├── number │ │ ├── __init__.py │ │ ├── intset_test.py │ │ ├── floatset_test.py │ │ └── intspan_test.py │ ├── text │ │ ├── __init__.py │ │ └── textset_test.py │ └── time │ │ ├── __init__.py │ │ ├── dateset_test.py │ │ └── tstzset_test.py └── conftest.py ├── pymeos ├── db │ ├── __init__.py │ ├── db_objects.py │ ├── asyncpg.py │ ├── psycopg2.py │ └── psycopg.py ├── collections │ ├── text │ │ ├── __init__.py │ │ └── textset.py │ ├── geo │ │ ├── __init__.py │ │ └── geoset.py │ ├── base │ │ ├── __init__.py │ │ └── collection.py │ ├── number │ │ ├── __init__.py │ │ └── intset.py │ ├── time │ │ ├── __init__.py │ │ ├── timecollection.py │ │ └── time.py │ └── __init__.py ├── boxes │ ├── __init__.py │ └── box.py ├── mixins │ ├── __init__.py │ ├── comparison.py │ ├── simplify.py │ └── temporal_comparison.py ├── temporal │ ├── __init__.py │ ├── interpolation.py │ ├── tinstant.py │ ├── tsequence.py │ └── tsequenceset.py ├── plotters │ ├── __init__.py │ ├── range_plotter.py │ ├── sequenceset_plotter.py │ ├── point_sequenceset_plotter.py │ ├── point_sequence_plotter.py │ ├── time_plotter.py │ ├── sequence_plotter.py │ └── box_plotter.py ├── aggregators │ ├── point_aggregators.py │ ├── bool_aggregators.py │ ├── text_aggregators.py │ ├── __init__.py │ ├── time_aggregators.py │ ├── number_aggregators.py │ ├── general_aggregators.py │ └── aggregator.py ├── main │ └── __init__.py ├── meos_init.py ├── __init__.py └── factory.py ├── dev-requirements.txt ├── docs ├── src │ ├── manual.rst │ ├── api │ │ ├── pymeos.meos_init.rst │ │ ├── pymeos.collections.text.rst │ │ ├── pymeos.collections.rst │ │ ├── pymeos.db.rst │ │ ├── pymeos.boxes.rst │ │ ├── pymeos.collections.base.rst │ │ ├── pymeos.collections.spatial.rst │ │ ├── pymeos.main.rst │ │ ├── pymeos.temporal.rst │ │ ├── pymeos.collections.numeric.rst │ │ ├── pymeos.collections.time.rst │ │ ├── pymeos.plotters.rst │ │ └── pymeos.aggregators.rst │ ├── examples.rst │ └── installation.rst ├── images │ ├── meos-logo.png │ ├── PyMEOS Logo.png │ └── PyMEOS Layer Architecture.png ├── requirements.txt ├── Makefile ├── make.bat ├── index.rst └── conf.py ├── .gitignore ├── .readthedocs.yaml ├── .github ├── workflows │ ├── black.yml │ └── test.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── pyproject.toml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pymeos/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/boxes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/main/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/temporal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/collections/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/collections/number/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/collections/text/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/collections/time/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | shapely 3 | geopandas 4 | pytest -------------------------------------------------------------------------------- /docs/src/manual.rst: -------------------------------------------------------------------------------- 1 | User Manual 2 | ==================== 3 | 4 | Section under construction -------------------------------------------------------------------------------- /docs/images/meos-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityDB/PyMEOS/HEAD/docs/images/meos-logo.png -------------------------------------------------------------------------------- /pymeos/collections/text/__init__.py: -------------------------------------------------------------------------------- 1 | from .textset import TextSet 2 | 3 | __all__ = ["TextSet"] 4 | -------------------------------------------------------------------------------- /docs/images/PyMEOS Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityDB/PyMEOS/HEAD/docs/images/PyMEOS Logo.png -------------------------------------------------------------------------------- /docs/images/PyMEOS Layer Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityDB/PyMEOS/HEAD/docs/images/PyMEOS Layer Architecture.png -------------------------------------------------------------------------------- /pymeos/boxes/__init__.py: -------------------------------------------------------------------------------- 1 | from .box import Box 2 | from .stbox import STBox 3 | from .tbox import TBox 4 | 5 | __all__ = ["Box", "TBox", "STBox"] 6 | -------------------------------------------------------------------------------- /pymeos/collections/geo/__init__.py: -------------------------------------------------------------------------------- 1 | from .geoset import GeoSet, GeometrySet, GeographySet 2 | 3 | __all__ = ["GeoSet", "GeometrySet", "GeographySet"] 4 | -------------------------------------------------------------------------------- /pymeos/collections/base/__init__.py: -------------------------------------------------------------------------------- 1 | from .set import Set 2 | from .span import Span 3 | from .spanset import SpanSet 4 | 5 | __all__ = ["Set", "Span", "SpanSet"] 6 | -------------------------------------------------------------------------------- /docs/src/api/pymeos.meos_init.rst: -------------------------------------------------------------------------------- 1 | Initialization 2 | ------------------------ 3 | 4 | .. automodule:: pymeos.meos_init 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /pymeos/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from .simplify import TSimplifiable 2 | from .comparison import TComparable 3 | from .temporal_comparison import TTemporallyEquatable, TTemporallyComparable 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | build 5 | dist 6 | docs/_build 7 | 8 | *.egg-info 9 | .pytest_cache 10 | *.pyc 11 | 12 | sandbox.py 13 | sandbox.ipynb 14 | 15 | coverage.xml -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | shapely 3 | pymeos_cffi 4 | asyncpg 5 | psycopg 6 | psycopg2 7 | matplotlib 8 | geopandas 9 | sphinx 10 | sphinx-book-theme 11 | myst_nb 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: "ubuntu-20.04" 4 | tools: 5 | python: "3.10" 6 | 7 | python: 8 | install: 9 | - requirements: docs/requirements.txt 10 | 11 | sphinx: 12 | configuration: docs/conf.py -------------------------------------------------------------------------------- /docs/src/api/pymeos.collections.text.rst: -------------------------------------------------------------------------------- 1 | Text Collections 2 | =================== 3 | 4 | TextSet 5 | ------------------------------- 6 | 7 | .. automodule:: pymeos.collections.text.textset 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pymeos import pymeos_initialize, pymeos_finalize 2 | 3 | 4 | def pytest_configure(config): 5 | pymeos_initialize("UTC") 6 | 7 | 8 | def pytest_unconfigure(config): 9 | pymeos_finalize() 10 | 11 | 12 | class TestPyMEOS: 13 | pass 14 | -------------------------------------------------------------------------------- /docs/src/api/pymeos.collections.rst: -------------------------------------------------------------------------------- 1 | Collections 2 | =================== 3 | 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | pymeos.collections.base 9 | pymeos.collections.time 10 | pymeos.collections.spatial 11 | pymeos.collections.numeric 12 | pymeos.collections.text 13 | 14 | -------------------------------------------------------------------------------- /pymeos/temporal/__init__.py: -------------------------------------------------------------------------------- 1 | from .interpolation import TInterpolation 2 | from .temporal import Temporal 3 | from .tinstant import TInstant 4 | from .tsequence import TSequence 5 | from .tsequenceset import TSequenceSet 6 | 7 | __all__ = ["TInterpolation", "Temporal", "TInstant", "TSequence", "TSequenceSet"] 8 | -------------------------------------------------------------------------------- /pymeos/collections/number/__init__.py: -------------------------------------------------------------------------------- 1 | from .intset import IntSet 2 | from .intspan import IntSpan 3 | from .intspanset import IntSpanSet 4 | from .floatset import FloatSet 5 | from .floatspan import FloatSpan 6 | from .floatspanset import FloatSpanSet 7 | 8 | __all__ = ["IntSet", "IntSpan", "IntSpanSet", "FloatSet", "FloatSpan", "FloatSpanSet"] 9 | -------------------------------------------------------------------------------- /pymeos/boxes/box.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from .stbox import STBox 4 | from .tbox import TBox 5 | 6 | Box = Union[TBox, STBox] 7 | """ 8 | Union type that includes all Box types in PyMEOS: 9 | 10 | - :class:`~pymeos.boxes.tbox.TBox` for numeric temporal boxes. 11 | - :class:`~pymeos.boxes.stbox.STBox` for spatio-temporal boxes. 12 | """ 13 | -------------------------------------------------------------------------------- /pymeos/plotters/__init__.py: -------------------------------------------------------------------------------- 1 | from .box_plotter import BoxPlotter 2 | from .point_sequence_plotter import TemporalPointSequencePlotter 3 | from .point_sequenceset_plotter import TemporalPointSequenceSetPlotter 4 | from .range_plotter import SpanPlotter 5 | from .sequence_plotter import TemporalSequencePlotter 6 | from .sequenceset_plotter import TemporalSequenceSetPlotter 7 | from .time_plotter import TimePlotter 8 | -------------------------------------------------------------------------------- /docs/src/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ==================== 3 | 4 | Section under construction. However, you can check the existing examples available in the 5 | `PyMEOS Examples repository `__ and the 6 | `PyMEOS-Demo repository `__. 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | :caption: Examples: 11 | 12 | examples/AIS.ipynb 13 | examples/BerlinMOD.ipynb -------------------------------------------------------------------------------- /docs/src/api/pymeos.db.rst: -------------------------------------------------------------------------------- 1 | DB 2 | ================= 3 | 4 | asyncpg 5 | ------------------------ 6 | 7 | .. automodule:: pymeos.db.asyncpg 8 | :members: 9 | :undoc-members: 10 | 11 | psycopg 12 | ------------------------ 13 | 14 | .. automodule:: pymeos.db.psycopg 15 | :members: 16 | :undoc-members: 17 | 18 | psycopg2 19 | ------------------------- 20 | 21 | .. automodule:: pymeos.db.psycopg2 22 | :members: 23 | :undoc-members: 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Lint code with Black 2 | 3 | on: 4 | push: 5 | branches: [ "master", "stable-[0-9]+.[0-9]+" ] 6 | pull_request: 7 | branches: [ "master", "stable-[0-9]+.[0-9]+" ] 8 | 9 | jobs: 10 | lint: 11 | name: Lint code with Black 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | 17 | - name: Run Black 18 | uses: psf/black@stable 19 | with: 20 | version: "~= 24.0" -------------------------------------------------------------------------------- /docs/src/api/pymeos.boxes.rst: -------------------------------------------------------------------------------- 1 | Boxes 2 | ==================== 3 | 4 | Box 5 | ----------------------- 6 | 7 | .. automodule:: pymeos.boxes.box 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | TBox 13 | ------------------------ 14 | 15 | .. automodule:: pymeos.boxes.tbox 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | STBox 21 | ------------------------- 22 | 23 | .. automodule:: pymeos.boxes.stbox 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /pymeos/collections/time/__init__.py: -------------------------------------------------------------------------------- 1 | from .tstzset import TsTzSet 2 | from .dateset import DateSet 3 | from .tstzspan import TsTzSpan 4 | from .datespan import DateSpan 5 | from .tstzspanset import TsTzSpanSet 6 | from .datespanset import DateSpanSet 7 | from .time import Time, TimeDate 8 | from datetime import datetime, timedelta, date 9 | 10 | __all__ = [ 11 | "TimeDate", 12 | "date", 13 | "DateSet", 14 | "DateSpan", 15 | "DateSpanSet", 16 | "Time", 17 | "datetime", 18 | "TsTzSet", 19 | "TsTzSpan", 20 | "TsTzSpanSet", 21 | "timedelta", 22 | ] 23 | -------------------------------------------------------------------------------- /docs/src/api/pymeos.collections.base.rst: -------------------------------------------------------------------------------- 1 | Base Abstract Collections 2 | ========================= 3 | 4 | Set 5 | ------------------------------- 6 | 7 | .. automodule:: pymeos.collections.base.set 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | Span 13 | ------------------------- 14 | 15 | .. automodule:: pymeos.collections.base.span 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | SpanSet 21 | ---------------------------- 22 | 23 | .. automodule:: pymeos.collections.base.spanset 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/src/api/pymeos.collections.spatial.rst: -------------------------------------------------------------------------------- 1 | Spatial Collections 2 | =================== 3 | 4 | 5 | GeoSet 6 | ----------------------- 7 | 8 | .. autoclass:: pymeos.collections.geo.geoset.GeoSet 9 | :members: 10 | :undoc-members: 11 | :show-inheritance: 12 | 13 | 14 | GeometrySet 15 | ----------------------- 16 | 17 | .. autoclass:: pymeos.collections.geo.geoset.GeometrySet 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | 23 | GeographySet 24 | ----------------------- 25 | 26 | .. autoclass:: pymeos.collections.geo.geoset.GeographySet 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /pymeos/collections/time/timecollection.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from datetime import date 3 | from typing import TypeVar 4 | 5 | from ..base.collection import Collection 6 | 7 | TimeClass = TypeVar("TimeClass", bound=date) 8 | 9 | 10 | class TimeCollection(Collection[TimeClass], ABC): 11 | def is_before(self, other) -> bool: 12 | return self.is_left(other) 13 | 14 | def is_over_or_before(self, other) -> bool: 15 | return self.is_over_or_left(other) 16 | 17 | def is_over_or_after(self, other) -> bool: 18 | return self.is_over_or_right(other) 19 | 20 | def is_after(self, other) -> bool: 21 | return self.is_right(other) 22 | -------------------------------------------------------------------------------- /pymeos/aggregators/point_aggregators.py: -------------------------------------------------------------------------------- 1 | from pymeos_cffi import * 2 | 3 | from .aggregator import BaseAggregator 4 | from ..boxes import STBox 5 | from ..main import TPoint 6 | 7 | 8 | class TemporalPointExtentAggregator(BaseAggregator[TPoint, STBox]): 9 | """ 10 | Spatiotemporal extent of aggregated temporal points, i.e. smallest :class:`~pymeos.time.boxes.STBox` that 11 | includes all aggregated temporal numbers. 12 | 13 | MEOS Functions: 14 | tpoint_extent_transfn, temporal_tagg_finalfn 15 | """ 16 | 17 | _add_function = tpoint_extent_transfn 18 | 19 | @classmethod 20 | def _finish(cls, state) -> STBox: 21 | return STBox(_inner=state) 22 | -------------------------------------------------------------------------------- /pymeos/collections/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from .number import * 3 | from .text import * 4 | from .time import * 5 | from .text import * 6 | from .geo import * 7 | 8 | __all__ = [ 9 | "Set", 10 | "Span", 11 | "SpanSet", 12 | "TimeDate", 13 | "date", 14 | "DateSet", 15 | "DateSpan", 16 | "DateSpanSet", 17 | "Time", 18 | "datetime", 19 | "TsTzSet", 20 | "TsTzSpan", 21 | "TsTzSpanSet", 22 | "timedelta", 23 | "TextSet", 24 | "GeoSet", 25 | "GeometrySet", 26 | "GeographySet", 27 | "IntSet", 28 | "IntSpan", 29 | "IntSpanSet", 30 | "FloatSet", 31 | "FloatSpan", 32 | "FloatSpanSet", 33 | ] 34 | -------------------------------------------------------------------------------- /pymeos/aggregators/bool_aggregators.py: -------------------------------------------------------------------------------- 1 | from pymeos_cffi import * 2 | 3 | from .aggregator import BaseAggregator 4 | from ..main import TBool 5 | 6 | 7 | class TemporalAndAggregator(BaseAggregator[TBool, TBool]): 8 | """ 9 | Temporal "and" of aggregated temporal booleans. 10 | 11 | MEOS Functions: 12 | tbool_tand_transfn, temporal_tagg_finalfn 13 | """ 14 | 15 | _add_function = tbool_tand_transfn 16 | 17 | 18 | class TemporalOrAggregator(BaseAggregator[TBool, TBool]): 19 | """ 20 | Temporal "or" of aggregated temporal booleans. 21 | 22 | MEOS Functions: 23 | tbool_tor_transfn, temporal_tagg_finalfn 24 | """ 25 | 26 | _add_function = tbool_tor_transfn 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /pymeos/aggregators/text_aggregators.py: -------------------------------------------------------------------------------- 1 | from pymeos_cffi import * 2 | 3 | from .aggregator import BaseAggregator 4 | from ..main import TText 5 | 6 | 7 | class TemporalTextMaxAggregator(BaseAggregator[TText, TText]): 8 | """ 9 | Temporal maximum of all aggregated temporal texts. 10 | 11 | MEOS Functions: 12 | ttext_tmax_transfn, temporal_tagg_finalfn 13 | """ 14 | 15 | _add_function = ttext_tmax_transfn 16 | 17 | 18 | class TemporalTextMinAggregator(BaseAggregator[TText, TText]): 19 | """ 20 | Temporal minimum of all aggregated temporal texts. 21 | 22 | MEOS Functions: 23 | ttext_tmin_transfn, temporal_tagg_finalfn 24 | """ 25 | 26 | _add_function = ttext_tmin_transfn 27 | -------------------------------------------------------------------------------- /docs/src/api/pymeos.main.rst: -------------------------------------------------------------------------------- 1 | Main 2 | =================== 3 | 4 | TBool 5 | ------------------------ 6 | 7 | .. automodule:: pymeos.main.tbool 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | TInt 13 | ----------------------- 14 | 15 | .. automodule:: pymeos.main.tint 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | TFloat 21 | ------------------------- 22 | 23 | .. automodule:: pymeos.main.tfloat 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | TPoint 29 | ------------------------- 30 | 31 | .. automodule:: pymeos.main.tpoint 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | TText 37 | ------------------------ 38 | 39 | .. automodule:: pymeos.main.ttext 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS] 28 | - Architecture: [e.g. x64] 29 | - Environment: [e.g. conda] 30 | - Version [e.g. 1.1.1] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /pymeos/db/db_objects.py: -------------------------------------------------------------------------------- 1 | from pymeos import ( 2 | TBool, 3 | TInt, 4 | TFloat, 5 | TText, 6 | TGeomPoint, 7 | TGeogPoint, 8 | TBox, 9 | STBox, 10 | TsTzSet, 11 | TsTzSpan, 12 | TsTzSpanSet, 13 | GeometrySet, 14 | GeographySet, 15 | TextSet, 16 | IntSet, 17 | IntSpan, 18 | IntSpanSet, 19 | FloatSpan, 20 | FloatSet, 21 | FloatSpanSet, 22 | ) 23 | 24 | db_objects = [ 25 | # Temporal 26 | TBool, 27 | TInt, 28 | TFloat, 29 | TText, 30 | TGeomPoint, 31 | TGeogPoint, 32 | # Boxes 33 | TBox, 34 | STBox, 35 | # Collections 36 | TsTzSet, 37 | TsTzSpan, 38 | TsTzSpanSet, 39 | GeometrySet, 40 | GeographySet, 41 | TextSet, 42 | IntSet, 43 | IntSpan, 44 | IntSpanSet, 45 | FloatSet, 46 | FloatSpan, 47 | FloatSpanSet, 48 | ] 49 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/src/api/pymeos.temporal.rst: -------------------------------------------------------------------------------- 1 | Temporal 2 | ======================= 3 | 4 | Temporal 5 | ------------------------------- 6 | 7 | .. automodule:: pymeos.temporal.temporal 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | TInstant 13 | ------------------------------- 14 | 15 | .. automodule:: pymeos.temporal.tinstant 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | TSequence 21 | -------------------------------- 22 | 23 | .. automodule:: pymeos.temporal.tsequence 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | TSequenceSet 29 | ----------------------------------- 30 | 31 | .. automodule:: pymeos.temporal.tsequenceset 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | TInterpolation 37 | ------------------------------------ 38 | 39 | .. automodule:: pymeos.temporal.interpolation 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | -------------------------------------------------------------------------------- /tests/temporal/interpolation_test.py: -------------------------------------------------------------------------------- 1 | from pymeos import TInterpolation 2 | from tests.conftest import TestPyMEOS 3 | import pytest 4 | 5 | 6 | class TestTInterpolation(TestPyMEOS): 7 | @pytest.mark.parametrize( 8 | "source, result", 9 | [ 10 | ("discrete", TInterpolation.DISCRETE), 11 | ("linear", TInterpolation.LINEAR), 12 | ("stepwise", TInterpolation.STEPWISE), 13 | ("step", TInterpolation.STEPWISE), 14 | ("none", TInterpolation.NONE), 15 | ], 16 | ids=["discrete", "linear", "stepwise", "step", "none"], 17 | ) 18 | def test_from_string(self, source, result): 19 | assert TInterpolation.from_string(source) == result 20 | 21 | def test_from_string_invalid(self): 22 | assert TInterpolation.from_string("invalid") == TInterpolation.NONE 23 | 24 | def test_from_string_invalid_none(self): 25 | with pytest.raises(ValueError): 26 | TInterpolation.from_string("invalid", none=False) 27 | -------------------------------------------------------------------------------- /pymeos/aggregators/__init__.py: -------------------------------------------------------------------------------- 1 | from .bool_aggregators import * 2 | from .general_aggregators import * 3 | from .number_aggregators import * 4 | from .text_aggregators import * 5 | from .point_aggregators import * 6 | from .time_aggregators import * 7 | 8 | __all__ = [ 9 | # General 10 | "TemporalInstantCountAggregator", 11 | "TemporalPeriodCountAggregator", 12 | "TemporalExtentAggregator", 13 | # Bool 14 | "TemporalAndAggregator", 15 | "TemporalOrAggregator", 16 | # Number 17 | "TemporalAverageAggregator", 18 | "TemporalNumberExtentAggregator", 19 | "TemporalIntMaxAggregator", 20 | "TemporalIntMinAggregator", 21 | "TemporalIntSumAggregator", 22 | "TemporalFloatMaxAggregator", 23 | "TemporalFloatMinAggregator", 24 | "TemporalFloatSumAggregator", 25 | # Text 26 | "TemporalTextMaxAggregator", 27 | "TemporalTextMinAggregator", 28 | # Point 29 | "TemporalPointExtentAggregator", 30 | # Time 31 | "TimeInstantaneousUnionAggregator", 32 | "TimeContinuousUnionAggregator", 33 | ] 34 | -------------------------------------------------------------------------------- /docs/src/api/pymeos.collections.numeric.rst: -------------------------------------------------------------------------------- 1 | Numeric Collections 2 | =================== 3 | 4 | IntSet 5 | ----------------------- 6 | 7 | .. automodule:: pymeos.collections.number.intset 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | 13 | FloatSet 14 | ----------------------- 15 | 16 | .. automodule:: pymeos.collections.number.floatset 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | 21 | IntSpan 22 | ----------------------- 23 | 24 | .. automodule:: pymeos.collections.number.intspan 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | 30 | FloatSpan 31 | ----------------------- 32 | 33 | .. automodule:: pymeos.collections.number.floatspan 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | IntSpanSet 39 | ----------------------- 40 | 41 | .. automodule:: pymeos.collections.number.intspanset 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | 47 | FloatSpanSet 48 | ----------------------- 49 | 50 | .. automodule:: pymeos.collections.number.floatspanset 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /pymeos/collections/time/time.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, date 2 | from typing import Union 3 | 4 | from .tstzspan import TsTzSpan 5 | from .tstzspanset import TsTzSpanSet 6 | from .tstzset import TsTzSet 7 | 8 | from .datespan import DateSpan 9 | from .datespanset import DateSpanSet 10 | from .dateset import DateSet 11 | 12 | Time = Union[ 13 | datetime, 14 | TsTzSet, 15 | TsTzSpan, 16 | TsTzSpanSet, 17 | ] 18 | """ 19 | Union type that includes all Time types related to timestamps in PyMEOS: 20 | 21 | - :class:`~datetime.datetime` for timestamps 22 | - :class:`~pymeos.time.tstzset.TsTzSet` for sets of timestamps 23 | - :class:`~pymeos.time.tstzspan.TsTzSpan` for spans of time 24 | - :class:`~pymeos.time.tstzspanset.TsTzSpanSet` for sets of spans of time 25 | """ 26 | 27 | TimeDate = Union[ 28 | date, 29 | DateSet, 30 | DateSpan, 31 | DateSpanSet, 32 | ] 33 | """ 34 | Union type that includes all Time types related to dates in PyMEOS: 35 | 36 | - :class:`~datetime.date` for dates 37 | - :class:`~pymeos.time.dateset.DateSet` for sets of dates 38 | - :class:`~pymeos.time.datespan.DateSpan` for spans of dates 39 | - :class:`~pymeos.time.datespanset.DateSpanSet` for sets of spans of dates 40 | """ 41 | -------------------------------------------------------------------------------- /pymeos/main/__init__.py: -------------------------------------------------------------------------------- 1 | from .tbool import TBool, TBoolInst, TBoolSeq, TBoolSeqSet 2 | from .tfloat import TFloat, TFloatInst, TFloatSeq, TFloatSeqSet 3 | from .tint import TInt, TIntInst, TIntSeq, TIntSeqSet 4 | from .tnumber import TNumber 5 | from .tpoint import ( 6 | TPoint, 7 | TPointInst, 8 | TPointSeq, 9 | TPointSeqSet, 10 | TGeomPoint, 11 | TGeomPointInst, 12 | TGeomPointSeq, 13 | TGeomPointSeqSet, 14 | TGeogPoint, 15 | TGeogPointInst, 16 | TGeogPointSeq, 17 | TGeogPointSeqSet, 18 | ) 19 | from .ttext import TText, TTextInst, TTextSeq, TTextSeqSet 20 | 21 | __all__ = [ 22 | "TBool", 23 | "TBoolInst", 24 | "TBoolSeq", 25 | "TBoolSeqSet", 26 | "TNumber", 27 | "TInt", 28 | "TIntInst", 29 | "TIntSeq", 30 | "TIntSeqSet", 31 | "TFloat", 32 | "TFloatInst", 33 | "TFloatSeq", 34 | "TFloatSeqSet", 35 | "TText", 36 | "TTextInst", 37 | "TTextSeq", 38 | "TTextSeqSet", 39 | "TPoint", 40 | "TPointInst", 41 | "TPointSeq", 42 | "TPointSeqSet", 43 | "TGeomPoint", 44 | "TGeomPointInst", 45 | "TGeomPointSeq", 46 | "TGeomPointSeqSet", 47 | "TGeogPoint", 48 | "TGeogPointInst", 49 | "TGeogPointSeq", 50 | "TGeogPointSeqSet", 51 | ] 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | PostgreSQL License 2 | 3 | ------------------------------------------------------------------------------- 4 | This PyMEOS code is provided under The PostgreSQL License. 5 | 6 | Copyright (c) 2020, Université libre de Bruxelles and PyMEOS contributors 7 | 8 | Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby 9 | granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. 10 | 11 | IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST 12 | PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 13 | DAMAGE. 14 | 15 | UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 16 | FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO PROVIDE 17 | MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 18 | ------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /docs/src/api/pymeos.collections.time.rst: -------------------------------------------------------------------------------- 1 | Time Collections 2 | =================== 3 | 4 | Time & TimeDate 5 | ----------------------- 6 | 7 | .. automodule:: pymeos.collections.time.time 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | TsTzSet 13 | ------------------------------- 14 | 15 | .. automodule:: pymeos.collections.time.tstzset 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | TsTzSpan 21 | ------------------------- 22 | 23 | .. automodule:: pymeos.collections.time.tstzspan 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | TsTzSpanSet 29 | ---------------------------- 30 | 31 | .. automodule:: pymeos.collections.time.tstzspanset 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | DateSet 37 | ------------------------------- 38 | 39 | .. automodule:: pymeos.collections.time.dateset 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | DateSpan 45 | ------------------------- 46 | 47 | .. automodule:: pymeos.collections.time.datespan 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | DateSpanSet 53 | ---------------------------- 54 | 55 | .. automodule:: pymeos.collections.time.datespanset 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | -------------------------------------------------------------------------------- /pymeos/plotters/range_plotter.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from matplotlib import pyplot as plt 4 | from pymeos import IntSpan, FloatSpan 5 | 6 | 7 | class SpanPlotter: 8 | """ 9 | Plotter for :class:`FloatSpan` and :class:`IntSpan` objects. 10 | """ 11 | 12 | @staticmethod 13 | def plot_span(span: Union[IntSpan, FloatSpan], *args, axes=None, **kwargs): 14 | """ 15 | Plot a :class:`FloatSpan` or :class:`IntSpan` on the given axes. 16 | 17 | Params: 18 | span: The :class:`FloatSpan` or :class:`IntSpan` to plot. 19 | axes: The axes to plot on. If None, the current axes are used. 20 | *args: Additional arguments to pass to the plot function. 21 | **kwargs: Additional keyword arguments to pass to the plot function. 22 | 23 | Returns: 24 | List with the plotted elements. 25 | """ 26 | base = axes or plt.gca() 27 | ll = base.axhline( 28 | span.lower(), *args, linestyle="-" if span.lower_inc() else "--", **kwargs 29 | ) 30 | kwargs.pop("label", None) 31 | ul = base.axhline( 32 | span.upper(), *args, linestyle="-" if span.upper_inc() else "--", **kwargs 33 | ) 34 | s = base.axhspan(span.lower(), span.upper(), *args, alpha=0.3, **kwargs) 35 | return [ll, ul, s] 36 | -------------------------------------------------------------------------------- /docs/src/api/pymeos.plotters.rst: -------------------------------------------------------------------------------- 1 | Plotters 2 | ======================= 3 | 4 | Time Plotter 5 | ------------------------------------ 6 | 7 | .. automodule:: pymeos.plotters.time_plotter 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | Range Plotter 13 | ------------------------------------- 14 | 15 | .. automodule:: pymeos.plotters.range_plotter 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | Sequence Plotter 21 | ---------------------------------------- 22 | 23 | .. automodule:: pymeos.plotters.sequence_plotter 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | SequenceSet Plotter 29 | ------------------------------------------- 30 | 31 | .. automodule:: pymeos.plotters.sequenceset_plotter 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | Point Sequence Plotter 37 | ----------------------------------------------- 38 | 39 | .. automodule:: pymeos.plotters.point_sequence_plotter 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | Point SequenceSet Plotter 45 | -------------------------------------------------- 46 | 47 | .. automodule:: pymeos.plotters.point_sequenceset_plotter 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | 53 | Box Plotter 54 | ----------------------------------- 55 | 56 | .. automodule:: pymeos.plotters.box_plotter 57 | :members: 58 | :undoc-members: 59 | :show-inheritance: -------------------------------------------------------------------------------- /docs/src/api/pymeos.aggregators.rst: -------------------------------------------------------------------------------- 1 | Aggregators 2 | ========================== 3 | 4 | Base Aggregation Classes 5 | ------------------------------------ 6 | 7 | .. automodule:: pymeos.aggregators.aggregator 8 | :members: 9 | :private-members: _add, _finish, _add_function, _final_function 10 | :undoc-members: 11 | :show-inheritance: 12 | 13 | Generic aggregators 14 | ---------------------------------------------- 15 | 16 | .. automodule:: pymeos.aggregators.general_aggregators 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | 21 | Time aggregators 22 | ------------------------------------------- 23 | 24 | .. automodule:: pymeos.aggregators.time_aggregators 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | Bool Aggregators 30 | ------------------------------------------- 31 | 32 | .. automodule:: pymeos.aggregators.bool_aggregators 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | 37 | Number aggregators 38 | --------------------------------------------- 39 | 40 | .. automodule:: pymeos.aggregators.number_aggregators 41 | :members: 42 | :undoc-members: 43 | :show-inheritance: 44 | 45 | Point Aggregators 46 | -------------------------------------------- 47 | 48 | .. automodule:: pymeos.aggregators.point_aggregators 49 | :members: 50 | :undoc-members: 51 | :show-inheritance: 52 | 53 | Text Aggregators 54 | ------------------------------------------- 55 | 56 | .. automodule:: pymeos.aggregators.text_aggregators 57 | :members: 58 | :undoc-members: 59 | :show-inheritance: 60 | -------------------------------------------------------------------------------- /pymeos/db/asyncpg.py: -------------------------------------------------------------------------------- 1 | import asyncpg 2 | 3 | from .db_objects import db_objects 4 | 5 | 6 | class MobilityDB: 7 | """ 8 | Helper class to register MobilityDB classes to an asyncpg connection and their automatic conversion to 9 | PyMEOS classes. 10 | """ 11 | 12 | @classmethod 13 | async def connect(cls, *args, **kwargs) -> asyncpg.connection.Connection: 14 | """ 15 | A coroutine to establish a connection to a MobilityDB server. 16 | 17 | Refer to :func:`asyncpg.connection.connect` for the list of valid arguments. 18 | 19 | If the connection already exists, see the :func:`~pymeos.db.asyncpg.MobilityDB.register` in this class. 20 | 21 | Args: 22 | *args: positional arguments that will be passed to :func:`asyncpg.connection.connect` 23 | **kwargs: keyword arguments that will be passed to :func:`asyncpg.connection.connect` 24 | 25 | Returns: 26 | A new :class:`asyncpg.connection.Connection` object with the MobilityDB classes registered. 27 | 28 | """ 29 | conn = await asyncpg.connect(*args, **kwargs) 30 | await cls.register(conn) 31 | return conn 32 | 33 | @classmethod 34 | async def register(cls, connection: asyncpg.connection.Connection) -> None: 35 | """ 36 | Registers MobilityDB classes to the passed connection. 37 | 38 | Args: 39 | connection: An :class:`asyncpg.connection.Connection` to register the classes to. 40 | """ 41 | for cl in db_objects: 42 | await connection.set_type_codec( 43 | cl._mobilitydb_name, encoder=str, decoder=cl.read_from_cursor 44 | ) 45 | -------------------------------------------------------------------------------- /pymeos/meos_init.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union, Tuple, Any 2 | 3 | from pymeos_cffi import ( 4 | meos_initialize, 5 | meos_finalize, 6 | meos_set_datestyle, 7 | meos_set_intervalstyle, 8 | ) 9 | 10 | 11 | def pymeos_initialize( 12 | timezone: Optional[str] = None, 13 | date_style: Union[None, str, Tuple[str, Any]] = None, 14 | interval_style: Union[None, str, Tuple[str, int]] = None, 15 | ) -> None: 16 | """ 17 | Initializes the underlying MEOS platform. 18 | Must be called before any other PyMEOS function. 19 | 20 | Args: 21 | timezone: :class:`str` indicating the desired timezone to be used. Defaults to system timezone. 22 | date_style: :class:`str` indicating the desired date style to be used. 23 | interval_style: :class:`str` indicating the desired interval style to be used. 24 | 25 | MEOS Functions: 26 | meos_initialize, meos_set_datestyle, meos_set_intervalstyle 27 | """ 28 | meos_initialize(timezone) 29 | 30 | if date_style is not None: 31 | if isinstance(date_style, str): 32 | meos_set_datestyle(date_style, None) 33 | else: 34 | meos_set_datestyle(*date_style) 35 | 36 | if interval_style is not None: 37 | if isinstance(interval_style, str): 38 | meos_set_intervalstyle(interval_style, None) 39 | else: 40 | meos_set_intervalstyle(*interval_style) 41 | 42 | 43 | def pymeos_finalize() -> None: 44 | """ 45 | Cleans up the underlying MEOS platform. 46 | Should be called at the end after PyMEOS is no longer used. 47 | 48 | MEOS Functions: 49 | meos_finalize 50 | """ 51 | meos_finalize() 52 | -------------------------------------------------------------------------------- /pymeos/plotters/sequenceset_plotter.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | from .sequence_plotter import TemporalSequencePlotter 4 | from .. import TSequence 5 | from ..temporal import TSequenceSet 6 | 7 | 8 | class TemporalSequenceSetPlotter: 9 | """ 10 | Plotter for :class:`TSequenceSet` and lists of :class:`TSequence`. 11 | """ 12 | 13 | @staticmethod 14 | def plot(sequence_set: Union[TSequenceSet, List[TSequence]], *args, **kwargs): 15 | """ 16 | Plot a :class:`TSequenceSet` or a list of :class:`TSequence` on the given axes. Every sequence in the set will be 17 | plotted with the same color. 18 | 19 | Params: 20 | sequence_set: The :class:`TSequenceSet` or list of :class:`TSequence` to plot. 21 | *args: Additional arguments to pass to the plot function. 22 | **kwargs: Additional keyword arguments to pass to the plot function. 23 | 24 | Returns: 25 | List with the plotted elements. 26 | 27 | See Also: 28 | :func:`~pymeos.plotters.sequence_plotter.TemporalSequencePlotter.plot` 29 | 30 | """ 31 | seqs = ( 32 | sequence_set.sequences() 33 | if isinstance(sequence_set, TSequenceSet) 34 | else sequence_set 35 | ) 36 | plots = [TemporalSequencePlotter.plot(seqs[0], *args, **kwargs)] 37 | if "color" not in kwargs: 38 | pl = plots[0] 39 | while isinstance(pl, list): 40 | pl = pl[0] 41 | kwargs["color"] = pl.get_color() 42 | kwargs.pop("label", None) 43 | for seq in seqs[1:]: 44 | plots.append(TemporalSequencePlotter.plot(seq, *args, **kwargs)) 45 | return plots 46 | -------------------------------------------------------------------------------- /pymeos/plotters/point_sequenceset_plotter.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List 2 | 3 | from .point_sequence_plotter import TemporalPointSequencePlotter 4 | from .. import TPointSeq 5 | from ..main import TPointSeqSet 6 | 7 | 8 | class TemporalPointSequenceSetPlotter: 9 | """ 10 | Plotter for :class:`TPointSeqSet` and lists of :class:`TPointSeq`. 11 | """ 12 | 13 | @staticmethod 14 | def plot_xy(sequence_set: Union[TPointSeqSet, List[TPointSeq]], *args, **kwargs): 15 | """ 16 | Plot a TPointSeqSet or a list of TPointSeq on the given axes. Every sequence in the set will be plotted with the 17 | same color. 18 | 19 | Params: 20 | sequence_set: The :class:`TPointSeqSet` or list of :class:`TPointSeq` to plot. 21 | *args: Additional arguments to pass to the plot function. 22 | **kwargs: Additional keyword arguments to pass to the plot function. 23 | 24 | Returns: 25 | List of lists with the plotted elements of each sequence. 26 | 27 | See Also: 28 | :func:`~pymeos.plotters.point_sequence_plotter.TemporalPointSequencePlotter.plot_xy`, 29 | :func:`~pymeos.plotters.point_sequence_plotter.TemporalPointSequencePlotter.plot_sequences_xy` 30 | """ 31 | seqs = ( 32 | sequence_set.sequences() 33 | if isinstance(sequence_set, TPointSeqSet) 34 | else sequence_set 35 | ) 36 | plots = [TemporalPointSequencePlotter.plot_xy(seqs[0], *args, **kwargs)] 37 | if "color" not in kwargs: 38 | kwargs["color"] = plots[0][0][0].get_color() 39 | kwargs.pop("label", None) 40 | for seq in seqs[1:]: 41 | plots.append(TemporalPointSequencePlotter.plot_xy(seq, *args, **kwargs)) 42 | return plots 43 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | PyMEOS 3 | ======= 4 | 5 | `MEOS (Mobility Engine, Open Source) `__ is a 6 | C library which enables the manipulation of temporal and spatio-temporal 7 | data based on `MobilityDB `__\ ’s data types 8 | and functions. 9 | 10 | PyMEOS is a library built on top of MEOS that provides all of its 11 | functionality wrapped in a set of Python classes. 12 | 13 | Requirements 14 | ============ 15 | 16 | PyMEOS 1.1 requires 17 | 18 | * Python >=3.7 19 | * MEOS >=1.1 20 | 21 | Installing PyMEOS 22 | ================== 23 | 24 | We recommend installing PyMEOS using one of the available built 25 | distributions using ``pip`` or ``conda``: 26 | 27 | Using ``pip``: 28 | 29 | .. code-block:: console 30 | 31 | pip install pymeos 32 | 33 | 34 | Using ``conda``: 35 | 36 | .. code-block:: console 37 | 38 | conda config --add channels conda-forge 39 | conda config --set channel_priority strict 40 | conda install -c conda-forge pymeos 41 | 42 | 43 | See the `installation documentation <./src/installation.html>`__ 44 | for more details and advanced installation instructions. 45 | 46 | Examples 47 | ================== 48 | A couple of examples showcasing the capabilities of PyMEOS can be found int the `examples section <./src/examples.html>`__. 49 | 50 | .. toctree:: 51 | :caption: User Guide 52 | :hidden: 53 | 54 | src/installation 55 | src/manual 56 | src/examples 57 | 58 | 59 | .. toctree:: 60 | :caption: API Reference 61 | :hidden: 62 | 63 | src/api/pymeos.meos_init 64 | src/api/pymeos.collections 65 | src/api/pymeos.temporal 66 | src/api/pymeos.main 67 | src/api/pymeos.boxes 68 | src/api/pymeos.aggregators 69 | src/api/pymeos.plotters 70 | src/api/pymeos.db 71 | 72 | Indices and tables 73 | ================== 74 | 75 | * :ref:`genindex` 76 | * :ref:`modindex` 77 | * :ref:`search` 78 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools>=61.0'] 3 | build-backend = 'setuptools.build_meta' 4 | 5 | [project] 6 | name = 'pymeos' 7 | dynamic = ['version'] 8 | authors = [ 9 | { name = 'Victor Divi', email = 'vdiviloper@gmail.com' }, 10 | { name = 'Zhicheng Luo', email = 'zhicheng.luo@ulb.be' }, 11 | { name = 'Krishna Chaitanya Bommakanti', email = 'bkchaitan94@gmail.com' }, 12 | ] 13 | description = 'Python wrapper for the MEOS C Library.' 14 | classifiers = [ 15 | 'License :: OSI Approved :: PostgreSQL License', 16 | 'Development Status :: 4 - Beta', 17 | 'Intended Audience :: Developers', 18 | 'Intended Audience :: Science/Research', 19 | 'Programming Language :: C', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.8', 23 | 'Programming Language :: Python :: 3.9', 24 | 'Programming Language :: Python :: 3.10', 25 | 'Programming Language :: Python :: 3.11', 26 | 'Programming Language :: Python :: 3 :: Only', 27 | 'Programming Language :: Python :: Implementation :: CPython', 28 | 'Operating System :: POSIX', 29 | 'Operating System :: Unix' 30 | ] 31 | readme = 'README.md' 32 | license = { file = 'LICENSE' } 33 | 34 | requires-python = '>=3.8' 35 | dependencies = [ 36 | 'pymeos-cffi >=1.1.0, <2', 37 | 'python-dateutil', 38 | 'shapely>=2.0.0', 39 | ] 40 | 41 | [project.optional-dependencies] 42 | dbp = [ 43 | 'psycopg' 44 | ] 45 | 46 | dbp2 = [ 47 | 'psycopg2' 48 | ] 49 | 50 | dba = [ 51 | 'asyncpg' 52 | ] 53 | 54 | plot = [ 55 | 'matplotlib' 56 | ] 57 | 58 | pandas = [ 59 | 'geopandas' 60 | ] 61 | 62 | [project.urls] 63 | 'Homepage' = 'https://github.com/MobilityDB/PyMEOS/pymeos' 64 | 'Bug Tracker' = 'https://github.com/MobilityDB/PyMEOS/issues' 65 | 'Changelog' = 'https://github.com/MobilityDB/PyMEOS/blob/master/pymeos/CHANGELOG.md' 66 | 67 | [tool.setuptools.dynamic] 68 | version = { attr = "pymeos.__version__" } 69 | -------------------------------------------------------------------------------- /pymeos/collections/base/collection.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from typing import Generic, TypeVar, Type 5 | 6 | T = TypeVar("T") 7 | Self = TypeVar("Self") 8 | 9 | 10 | class Collection(Generic[T], ABC): 11 | # ------------------------- Topological Operations ------------------------ 12 | # @abstractmethod 13 | # def is_adjacent(self, other) -> bool: 14 | # raise NotImplementedError() 15 | 16 | @abstractmethod 17 | def is_contained_in(self, container) -> bool: 18 | raise NotImplementedError() 19 | 20 | @abstractmethod 21 | def contains(self, content) -> bool: 22 | raise NotImplementedError() 23 | 24 | @abstractmethod 25 | def __contains__(self, item): 26 | raise NotImplementedError() 27 | 28 | @abstractmethod 29 | def overlaps(self, other) -> bool: 30 | raise NotImplementedError() 31 | 32 | # ------------------------- Position Operations --------------------------- 33 | @abstractmethod 34 | def is_left(self, other) -> bool: 35 | raise NotImplementedError() 36 | 37 | @abstractmethod 38 | def is_over_or_left(self, other) -> bool: 39 | raise NotImplementedError() 40 | 41 | @abstractmethod 42 | def is_over_or_right(self, other) -> bool: 43 | raise NotImplementedError() 44 | 45 | @abstractmethod 46 | def is_right(self, other) -> bool: 47 | raise NotImplementedError() 48 | 49 | # ------------------------- Database Operations --------------------------- 50 | 51 | # ------------------------- Database Operations --------------------------- 52 | @classmethod 53 | def read_from_cursor(cls: Type[Self], value, _=None): 54 | """ 55 | Reads a :class:`Collection` from a database cursor. Used when automatically 56 | loading objects from the database. 57 | Users should use the class constructor instead. 58 | """ 59 | if not value: 60 | return None 61 | return cls(string=value) 62 | -------------------------------------------------------------------------------- /pymeos/db/psycopg2.py: -------------------------------------------------------------------------------- 1 | import psycopg2 2 | from psycopg2 import extensions, connect 3 | 4 | from .db_objects import db_objects 5 | 6 | 7 | class MobilityDB: 8 | """ 9 | Helper class to register MobilityDB classes to a psycopg2 connection and their automatic conversion to 10 | PyMEOS classes. 11 | """ 12 | 13 | @classmethod 14 | def connect(cls, *args, **kwargs) -> psycopg2.extensions.connection: 15 | """ 16 | Establishes a connection to a MobilityDB server. 17 | 18 | Refer to :func:`psycopg2.connect` for the list of valid arguments. 19 | 20 | If the connection already exists, see the :func:`~pymeos.db.psycopg2.MobilityDB.register` in this class. 21 | 22 | Args: 23 | *args: positional arguments that will be passed to :func:`psycopg.connect` 24 | **kwargs: keyword arguments that will be passed to :func:`psycopg.connect` 25 | 26 | Returns: 27 | A new :class:`psycopg2.extensions.connection` object with the MobilityDB classes registered. 28 | 29 | """ 30 | conn = connect(*args, **kwargs) 31 | cls.register(conn) 32 | return conn 33 | 34 | @classmethod 35 | def register(cls, connection: psycopg2.extensions.connection) -> None: 36 | """ 37 | Registers MobilityDB classes to the passed connection. 38 | 39 | Args: 40 | connection: An :class:`psycopg2.extensions.connection` to register the classes to. 41 | """ 42 | if isinstance(connection, extensions.cursor): 43 | # Retro compatibility 44 | cursor = connection 45 | else: 46 | cursor = connection.cursor() 47 | 48 | # Add MobilityDB types to PostgreSQL adapter and specify the reader function for each type. 49 | for cl in db_objects: 50 | cursor.execute(f"SELECT NULL::{cl._mobilitydb_name}") 51 | oid = cursor.description[0][1] 52 | extensions.register_type( 53 | extensions.new_type((oid,), cl._mobilitydb_name, cl.read_from_cursor) 54 | ) 55 | -------------------------------------------------------------------------------- /pymeos/temporal/interpolation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from pymeos_cffi import InterpolationType 3 | from enum import IntEnum 4 | 5 | 6 | class TInterpolation(IntEnum): 7 | """ 8 | Enum for representing the different types of interpolation present in 9 | PyMEOS. 10 | """ 11 | 12 | NONE = InterpolationType.NONE 13 | DISCRETE = InterpolationType.DISCRETE 14 | STEPWISE = InterpolationType.STEP 15 | LINEAR = InterpolationType.LINEAR 16 | 17 | def to_string(self) -> str: 18 | """ 19 | Returns a string representation of the interpolation type. 20 | """ 21 | 22 | if self == InterpolationType.NONE: 23 | return "None" 24 | elif self == InterpolationType.DISCRETE: 25 | return "Discrete" 26 | elif self == InterpolationType.STEP: 27 | return "Step" 28 | elif self == InterpolationType.LINEAR: 29 | return "Linear" 30 | 31 | @staticmethod 32 | def from_string(source: str, none: bool = True) -> TInterpolation: 33 | """ 34 | Returns the :class:`TInterpolation` element equivalent to `source`. 35 | 36 | Args: 37 | source: :class:`string` representing the interpolation 38 | none: indicates whether to return `TIntepolation.NONE` when 39 | `source` represents an invalid interpolation 40 | 41 | Returns: 42 | A new :class:`TInterpolation` instance. 43 | 44 | Raises: 45 | ValueError: when `source` doesn't represent any valid interpolation 46 | and `none` is False 47 | 48 | """ 49 | if source.lower() == "discrete": 50 | return TInterpolation.DISCRETE 51 | elif source.lower() == "linear": 52 | return TInterpolation.LINEAR 53 | elif source.lower() == "stepwise" or source.lower() == "step": 54 | return TInterpolation.STEPWISE 55 | elif source.lower() == "none" or none: 56 | return TInterpolation.NONE 57 | else: 58 | raise ValueError(f"Value {source} doesn't represent a valid interpolation") 59 | -------------------------------------------------------------------------------- /pymeos/db/psycopg.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import psycopg 4 | from psycopg import connect 5 | from psycopg.adapt import Loader, Buffer, Dumper 6 | 7 | from .db_objects import db_objects 8 | 9 | 10 | def _pymeos_loader_factory(cl): 11 | class _PymeosLoader(Loader): 12 | def load(self, data: Buffer) -> Any: 13 | return cl.read_from_cursor(data.decode()) 14 | 15 | return _PymeosLoader 16 | 17 | 18 | class _PymeosDumper(Dumper): 19 | def dump(self, obj: Any) -> Buffer: 20 | return str(obj).encode() 21 | 22 | 23 | class MobilityDB: 24 | """ 25 | Helper class to register MobilityDB classes to a psycopg (3) connection and their automatic conversion to 26 | PyMEOS classes. 27 | """ 28 | 29 | @classmethod 30 | def connect(cls, *args, **kwargs): 31 | """ 32 | Establishes a connection to a MobilityDB server. 33 | 34 | Refer to :func:`psycopg.connect` for the list of valid arguments. 35 | 36 | If the connection already exists, see the :func:`~pymeos.db.psycopg.MobilityDB.register` in this class. 37 | 38 | Args: 39 | *args: positional arguments that will be passed to :func:`psycopg.connect` 40 | **kwargs: keyword arguments that will be passed to :func:`psycopg.connect` 41 | 42 | Returns: 43 | A new :class:`psycopg.Connection` object with the MobilityDB classes registered. 44 | 45 | """ 46 | connection = connect(*args, **kwargs) 47 | cls.register(connection) 48 | return connection 49 | 50 | @classmethod 51 | def register(cls, connection: psycopg.Connection): 52 | """ 53 | Registers MobilityDB classes to the passed connection. 54 | 55 | Args: 56 | connection: An :class:`psycopg.Connection` to register the classes to. 57 | """ 58 | cursor = connection.cursor() 59 | for cl in db_objects: 60 | cursor.execute(f"SELECT NULL::{cl._mobilitydb_name}") 61 | oid = cursor.description[0][1] 62 | connection.adapters.register_loader(oid, _pymeos_loader_factory(cl)) 63 | connection.adapters.register_dumper(cl, _PymeosDumper) 64 | -------------------------------------------------------------------------------- /pymeos/aggregators/time_aggregators.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Union 3 | 4 | from pymeos_cffi import ( 5 | timestamptz_union_transfn, 6 | datetime_to_timestamptz, 7 | set_union_transfn, 8 | set_union_finalfn, 9 | union_spanset_span, 10 | union_spanset_spanset, 11 | ) 12 | 13 | from .aggregator import BaseAggregator 14 | from ..collections import TsTzSet, TsTzSpan, TsTzSpanSet 15 | 16 | 17 | class TimeInstantaneousUnionAggregator( 18 | BaseAggregator[Union[datetime, TsTzSet], TsTzSet] 19 | ): 20 | """ 21 | Temporal union of instantaneous time objects (:class:'~datetime.datetime' and 22 | :class:`~pymeos.time.tstzset.TsTzSet`). 23 | 24 | MEOS Functions: 25 | timestamp_union_transfn, set_union_transfn, set_union_finalfn 26 | """ 27 | 28 | @classmethod 29 | def _add(cls, state, temporal): 30 | if isinstance(temporal, datetime): 31 | state = timestamptz_union_transfn(state, datetime_to_timestamptz(temporal)) 32 | elif isinstance(temporal, TsTzSet): 33 | state = set_union_transfn(state, temporal._inner) 34 | else: 35 | cls._error(temporal) 36 | return state 37 | 38 | @classmethod 39 | def _finish(cls, state) -> TsTzSet: 40 | result = set_union_finalfn(state) 41 | return TsTzSet(_inner=result) 42 | 43 | 44 | class TimeContinuousUnionAggregator( 45 | BaseAggregator[Union[TsTzSpan, TsTzSpanSet], TsTzSpanSet] 46 | ): 47 | """ 48 | Temporal union of continuous time objects (:class:`~pymeos.time.tstzspan.TsTzSpan` and 49 | :class:`~pymeos.time.tstzspanset.TsTzSpanSet`). 50 | 51 | MEOS Functions: 52 | union_spanset_span, union_spanset_spanset 53 | """ 54 | 55 | @classmethod 56 | def _add(cls, state, temporal): 57 | if isinstance(temporal, TsTzSpan): 58 | state = union_spanset_span(state, temporal._inner) 59 | elif isinstance(temporal, TsTzSpanSet): 60 | state = union_spanset_spanset(state, temporal._inner) 61 | else: 62 | cls._error(temporal) 63 | return state 64 | 65 | @classmethod 66 | def _finish(cls, state) -> TsTzSpanSet: 67 | return TsTzSpanSet(_inner=state) 68 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "PyMEOS" 10 | copyright = "2023, Víctor Diví" 11 | author = "Víctor Diví" 12 | release = "1.1.3" 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | import os 17 | import sys 18 | 19 | sys.path.insert(0, os.path.abspath("../pymeos_cffi")) 20 | sys.path.insert(0, os.path.abspath("../pymeos")) 21 | 22 | extensions = [ 23 | "sphinx.ext.autodoc", 24 | "sphinx.ext.napoleon", 25 | "sphinx.ext.intersphinx", 26 | "myst_nb", 27 | ] 28 | 29 | nb_execution_mode = "off" 30 | 31 | templates_path = ["_templates"] 32 | exclude_patterns = [ 33 | "_build", 34 | "Thumbs.db", 35 | ".DS_Store", 36 | "_build", 37 | "**.ipynb_checkpoints", 38 | ] 39 | autodoc_member_order = "bysource" 40 | 41 | # -- Intersphinx config -------- 42 | intersphinx_mapping = { 43 | "asyncpg": ("https://magicstack.github.io/asyncpg/current/", None), 44 | "psycopg": ("https://www.psycopg.org/psycopg3/docs/", None), 45 | "psycopg2": ("https://www.psycopg.org/docs/", None), 46 | "shapely": ("https://shapely.readthedocs.io/en/stable/", None), 47 | "python": ("https://docs.python.org/3", None), 48 | } 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 52 | 53 | html_theme = "sphinx_book_theme" 54 | html_static_path = ["_static"] 55 | 56 | import requests 57 | 58 | 59 | def download_file(url, dest_path): 60 | response = requests.get(url, stream=True) 61 | response.raise_for_status() # Ensure we got a successful response 62 | 63 | # Ensure folder for destination file exists 64 | os.makedirs(os.path.dirname(dest_path), exist_ok=True) 65 | 66 | with open(dest_path, "wb") as file: 67 | for chunk in response.iter_content(chunk_size=8192): 68 | file.write(chunk) 69 | 70 | 71 | prefix = "https://raw.githubusercontent.com/MobilityDB/PyMEOS-Examples/main/" 72 | download_file(f"{prefix}PyMEOS_Examples/AIS.ipynb", "src/examples/AIS.ipynb") 73 | download_file( 74 | f"{prefix}PyMEOS_Examples/BerlinMOD.ipynb", "src/examples/BerlinMOD.ipynb" 75 | ) 76 | -------------------------------------------------------------------------------- /pymeos/aggregators/number_aggregators.py: -------------------------------------------------------------------------------- 1 | from pymeos_cffi import * 2 | 3 | from .aggregator import BaseAggregator 4 | from ..boxes import TBox 5 | from ..main import TInt, TFloat, TNumber 6 | 7 | 8 | class TemporalAverageAggregator(BaseAggregator[TNumber, TNumber]): 9 | """ 10 | Temporal average of aggregated temporal numbers. Accepts both Temporal Integers and Temporal floats 11 | 12 | MEOS Functions: 13 | tnumber_tavg_transfn, tnumber_tavg_finalfn 14 | """ 15 | 16 | _add_function = tnumber_tavg_transfn 17 | _final_function = tnumber_tavg_finalfn 18 | 19 | 20 | class TemporalNumberExtentAggregator(BaseAggregator[TNumber, TBox]): 21 | """ 22 | Temporal and numeric extent of aggregated temporal numbers, i.e. smallest :class:`~pymeos.time.boxes.TBox` that 23 | includes all aggregated temporal numbers. 24 | 25 | MEOS Functions: 26 | tnumber_extent_transfn, temporal_tagg_finalfn 27 | """ 28 | 29 | _add_function = tnumber_extent_transfn 30 | 31 | @classmethod 32 | def _finish(cls, state) -> TBox: 33 | return TBox(_inner=state) 34 | 35 | 36 | class TemporalIntMaxAggregator(BaseAggregator[TInt, TInt]): 37 | """ 38 | Temporal maximum of all aggregated temporal integers. 39 | 40 | MEOS Functions: 41 | tint_tmax_transfn, temporal_tagg_finalfn 42 | """ 43 | 44 | _add_function = tint_tmax_transfn 45 | 46 | 47 | class TemporalIntMinAggregator(BaseAggregator[TInt, TInt]): 48 | """ 49 | Temporal minimum of all aggregated temporal integers. 50 | 51 | MEOS Functions: 52 | tint_tmin_transfn, temporal_tagg_finalfn 53 | """ 54 | 55 | _add_function = tint_tmin_transfn 56 | 57 | 58 | class TemporalIntSumAggregator(BaseAggregator[TInt, TInt]): 59 | """ 60 | Temporal summ of all aggregated temporal integers. 61 | 62 | MEOS Functions: 63 | tint_tsum_transfn, temporal_tagg_finalfn 64 | """ 65 | 66 | _add_function = tint_tsum_transfn 67 | 68 | 69 | class TemporalFloatMaxAggregator(BaseAggregator[TFloat, TFloat]): 70 | """ 71 | Temporal maximum of all aggregated temporal floats. 72 | 73 | MEOS Functions: 74 | tfloat_tmax_transfn, temporal_tagg_finalfn 75 | """ 76 | 77 | _add_function = tfloat_tmax_transfn 78 | 79 | 80 | class TemporalFloatMinAggregator(BaseAggregator[TFloat, TFloat]): 81 | """ 82 | Temporal minimum of all aggregated temporal floats. 83 | 84 | MEOS Functions: 85 | tfloat_tmin_transfn, temporal_tagg_finalfn 86 | """ 87 | 88 | _add_function = tfloat_tmin_transfn 89 | 90 | 91 | class TemporalFloatSumAggregator(BaseAggregator[TFloat, TFloat]): 92 | """ 93 | Temporal summ of all aggregated temporal floats. 94 | 95 | MEOS Functions: 96 | tfloat_tsum_transfn, temporal_tagg_finalfn 97 | """ 98 | 99 | _add_function = tfloat_tsum_transfn 100 | -------------------------------------------------------------------------------- /pymeos/mixins/comparison.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from pymeos_cffi import ( 4 | temporal_eq, 5 | temporal_ne, 6 | temporal_lt, 7 | temporal_le, 8 | temporal_gt, 9 | temporal_ge, 10 | ) 11 | 12 | Self = TypeVar("Self", bound="Temporal[Any]") 13 | 14 | 15 | class TComparable: 16 | def __eq__(self: Self, other): 17 | """ 18 | Returns whether `self` is equal to `other`. 19 | 20 | Args: 21 | other: A temporal object to compare to `self`. 22 | 23 | Returns: 24 | A :class:`bool` with the result of the equality relation. 25 | 26 | MEOS Functions: 27 | temporal_eq 28 | """ 29 | return temporal_eq(self._inner, other._inner) 30 | 31 | def __ne__(self: Self, other): 32 | """ 33 | Returns whether `self` is not equal to `other`. 34 | 35 | Args: 36 | other: A temporal object to compare to `self`. 37 | 38 | Returns: 39 | A :class:`bool` with the result of the not equal relation. 40 | 41 | MEOS Functions: 42 | temporal_ne 43 | """ 44 | return temporal_ne(self._inner, other._inner) 45 | 46 | def __lt__(self: Self, other): 47 | """ 48 | Returns whether `self` is less than `other`. 49 | 50 | Args: 51 | other: A temporal object to compare to `self`. 52 | 53 | Returns: 54 | A :class:`bool` with the result of the less than relation. 55 | 56 | MEOS Functions: 57 | temporal_lt 58 | """ 59 | return temporal_lt(self._inner, other._inner) 60 | 61 | def __le__(self: Self, other): 62 | """ 63 | Returns whether `self` is less or equal than `other`. 64 | 65 | Args: 66 | other: A temporal object to compare to `self`. 67 | 68 | Returns: 69 | A :class:`bool` with the result of the less or equal than relation. 70 | 71 | MEOS Functions: 72 | temporal_le 73 | """ 74 | return temporal_le(self._inner, other._inner) 75 | 76 | def __gt__(self: Self, other): 77 | """ 78 | Returns whether `self` is greater than `other`. 79 | 80 | Args: 81 | other: A temporal object to compare to `self`. 82 | 83 | Returns: 84 | A :class:`bool` with the result of the greater than relation. 85 | 86 | MEOS Functions: 87 | temporal_gt 88 | """ 89 | return temporal_gt(self._inner, other._inner) 90 | 91 | def __ge__(self: Self, other): 92 | """ 93 | Returns whether `self` is greater or equal than `other`. 94 | 95 | Args: 96 | other: A temporal object to compare to `self`. 97 | 98 | Returns: 99 | A :class:`bool` with the result of the greater or equal than 100 | relation. 101 | 102 | MEOS Functions: 103 | temporal_ge 104 | """ 105 | return temporal_ge(self._inner, other._inner) 106 | -------------------------------------------------------------------------------- /pymeos/mixins/simplify.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from typing import TypeVar 3 | 4 | from pymeos_cffi import ( 5 | temporal_simplify_min_dist, 6 | temporal_simplify_min_tdelta, 7 | timedelta_to_interval, 8 | temporal_simplify_dp, 9 | temporal_simplify_max_dist, 10 | ) 11 | 12 | Self = TypeVar("Self", bound="Temporal[Any]") 13 | 14 | 15 | class TSimplifiable: 16 | def simplify_min_distance(self: Self, distance: float) -> Self: 17 | """ 18 | Simplifies a temporal value ensuring that consecutive values are at least a 19 | certain distance apart. 20 | 21 | Args: 22 | distance: :class:`float` indicating the minimum distance between two points. 23 | 24 | Returns: 25 | :class:`Temporal` with the same subtype as the input. 26 | 27 | MEOS Functions: 28 | temporal_simplify_min_dist 29 | """ 30 | return self.__class__(_inner=temporal_simplify_min_dist(self._inner, distance)) 31 | 32 | def simplify_min_tdelta(self: Self, distance: timedelta) -> Self: 33 | """ 34 | Simplifies a temporal value ensuring that consecutive values are at least a 35 | certain time apart. 36 | 37 | Args: 38 | distance: :class:`timedelta` indicating the minimum time between two points. 39 | 40 | Returns: 41 | :class:`Temporal` with the same subtype as the input. 42 | 43 | MEOS Functions: 44 | temporal_simplify_min_tdelta 45 | """ 46 | delta = timedelta_to_interval(distance) 47 | return self.__class__(_inner=temporal_simplify_min_tdelta(self._inner, delta)) 48 | 49 | def simplify_douglas_peucker( 50 | self: Self, distance: float, synchronized: bool = False 51 | ) -> Self: 52 | """ 53 | Simplifies a temporal value using the Douglas-Peucker line simplification 54 | algorithm. 55 | 56 | Args: 57 | distance: :class:`float` indicating the minimum distance between two points. 58 | synchronized: If `True`, the Synchronized Distance will be used. Otherwise, 59 | the spatial-only distance will be used. 60 | 61 | Returns: 62 | :class:`Temporal` with the same subtype as the input. 63 | 64 | MEOS Functions: 65 | temporal_simplify_dp 66 | """ 67 | return self.__class__( 68 | _inner=temporal_simplify_dp(self._inner, distance, synchronized) 69 | ) 70 | 71 | def simplify_max_distance( 72 | self: Self, distance: float, synchronized: bool = False 73 | ) -> Self: 74 | """ 75 | Simplifies a temporal value using a single-pass Douglas-Peucker line 76 | simplification algorithm. 77 | 78 | Args: 79 | distance: :class:`float` indicating the minimum distance between two points. 80 | synchronized: If `True`, the Synchronized Distance will be used. Otherwise, 81 | the spatial-only distance will be used. 82 | 83 | Returns: 84 | :class:`Temporal` with the same subtype as the input. 85 | 86 | MEOS Functions: 87 | temporal_simplify_max_dist 88 | """ 89 | return self.__class__( 90 | _inner=temporal_simplify_max_dist(self._inner, distance, synchronized) 91 | ) 92 | -------------------------------------------------------------------------------- /pymeos/temporal/tinstant.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from datetime import datetime 5 | from typing import Optional, Union, TypeVar, List 6 | 7 | from pymeos_cffi import * 8 | 9 | from .temporal import Temporal 10 | 11 | TBase = TypeVar("TBase") 12 | TG = TypeVar("TG", bound="Temporal[Any]") 13 | TI = TypeVar("TI", bound="TInstant[Any]") 14 | TS = TypeVar("TS", bound="TSequence[Any]") 15 | TSS = TypeVar("TSS", bound="TSequenceSet[Any]") 16 | Self = TypeVar("Self", bound="TInstant[Any]") 17 | 18 | 19 | class TInstant(Temporal[TBase, TG, TI, TS, TSS], ABC): 20 | """ 21 | Base class for temporal instant types, i.e. temporal values that are 22 | defined at a single point in time. 23 | """ 24 | 25 | __slots__ = ["_inner"] 26 | 27 | _make_function = None 28 | _cast_function = None 29 | 30 | def __init__( 31 | self, 32 | string: Optional[str] = None, 33 | *, 34 | value: Optional[Union[str, TBase]] = None, 35 | timestamp: Optional[Union[str, datetime]] = None, 36 | _inner=None, 37 | ): 38 | assert (_inner is not None) or ( 39 | (string is not None) != (value is not None and timestamp is not None) 40 | ), "Either string must be not None or both point and timestamp must be not" 41 | if _inner is not None: 42 | self._inner = as_tinstant(_inner) 43 | elif string is not None: 44 | self._inner = as_tinstant(self.__class__._parse_function(string)) 45 | else: 46 | ts = ( 47 | datetime_to_timestamptz(timestamp) 48 | if isinstance(timestamp, datetime) 49 | else pg_timestamptz_in(timestamp, -1) 50 | ) 51 | self._inner = self.__class__._make_function( 52 | self.__class__._cast_function(value), ts 53 | ) 54 | 55 | def value(self) -> TBase: 56 | """ 57 | Returns the value of the temporal instant. 58 | 59 | Returns: 60 | The value of the temporal instant. 61 | """ 62 | return self.start_value() 63 | 64 | def timestamp(self) -> datetime: 65 | """ 66 | Returns the timestamp of the temporal instant. 67 | 68 | Returns: 69 | A :class:`~datetime.datetime` object. 70 | 71 | MEOS Functions: 72 | temporal_timestamps 73 | """ 74 | ts, count = temporal_timestamps(self._inner) 75 | assert count == 1 76 | return timestamptz_to_datetime(ts[0]) 77 | 78 | def start_instant(self: Self) -> Self: 79 | return self 80 | 81 | def end_instant(self: Self) -> Self: 82 | return self 83 | 84 | def instant_n(self: Self, n: int) -> Self: 85 | if n == 0: 86 | return self 87 | else: 88 | raise Exception("ERROR: Out of range") 89 | 90 | def instants(self: Self) -> List[Self]: 91 | return [self] 92 | 93 | def start_timestamp(self) -> datetime: 94 | return self.timestamp() 95 | 96 | def end_timestamp(self) -> datetime: 97 | return self.timestamp() 98 | 99 | def timestamp_n(self, n) -> datetime: 100 | if n == 0: 101 | return self.timestamp() 102 | else: 103 | raise Exception("ERROR: Out of range") 104 | 105 | def timestamps(self) -> List[datetime]: 106 | return [self.timestamp()] 107 | -------------------------------------------------------------------------------- /pymeos/__init__.py: -------------------------------------------------------------------------------- 1 | from .aggregators import * 2 | from .boxes import * 3 | from .main import * 4 | from .meos_init import * 5 | from .temporal import * 6 | from .collections import * 7 | from pymeos_cffi import ( 8 | MeosException, 9 | MeosInternalError, 10 | MeosArgumentError, 11 | MeosIoError, 12 | MeosInternalTypeError, 13 | MeosValueOutOfRangeError, 14 | MeosDivisionByZeroError, 15 | MeosMemoryAllocError, 16 | MeosAggregationError, 17 | MeosDirectoryError, 18 | MeosFileError, 19 | MeosInvalidArgError, 20 | MeosInvalidArgTypeError, 21 | MeosInvalidArgValueError, 22 | MeosMfJsonInputError, 23 | MeosMfJsonOutputError, 24 | MeosTextInputError, 25 | MeosTextOutputError, 26 | MeosWkbInputError, 27 | MeosWkbOutputError, 28 | MeosGeoJsonInputError, 29 | MeosGeoJsonOutputError, 30 | ) 31 | 32 | __version__ = "1.3.0-alpha-1" 33 | __all__ = [ 34 | # initialization 35 | "pymeos_initialize", 36 | "pymeos_finalize", 37 | # boxes 38 | "Box", 39 | "TBox", 40 | "STBox", 41 | # main 42 | "TBool", 43 | "TBoolInst", 44 | "TBoolSeq", 45 | "TBoolSeqSet", 46 | "TInt", 47 | "TIntInst", 48 | "TIntSeq", 49 | "TIntSeqSet", 50 | "TFloat", 51 | "TFloatInst", 52 | "TFloatSeq", 53 | "TFloatSeqSet", 54 | "TText", 55 | "TTextInst", 56 | "TTextSeq", 57 | "TTextSeqSet", 58 | "TPointInst", 59 | "TPointSeq", 60 | "TPointSeqSet", 61 | "TGeomPoint", 62 | "TGeomPointInst", 63 | "TGeomPointSeq", 64 | "TGeomPointSeqSet", 65 | "TGeogPoint", 66 | "TGeogPointInst", 67 | "TGeogPointSeq", 68 | "TGeogPointSeqSet", 69 | # temporal 70 | "Temporal", 71 | "TInstant", 72 | "TSequence", 73 | "TSequenceSet", 74 | # Collections 75 | "Time", 76 | "TsTzSpan", 77 | "TsTzSet", 78 | "TsTzSpanSet", 79 | "TextSet", 80 | "IntSet", 81 | "IntSpan", 82 | "IntSpanSet", 83 | "FloatSet", 84 | "FloatSpan", 85 | "FloatSpanSet", 86 | "GeoSet", 87 | "GeometrySet", 88 | "GeographySet", 89 | # extras 90 | "TInterpolation", 91 | # aggregators 92 | "TemporalInstantCountAggregator", 93 | "TemporalPeriodCountAggregator", 94 | "TemporalExtentAggregator", 95 | "TemporalAndAggregator", 96 | "TemporalOrAggregator", 97 | "TemporalAverageAggregator", 98 | "TemporalNumberExtentAggregator", 99 | "TemporalIntMaxAggregator", 100 | "TemporalIntMinAggregator", 101 | "TemporalIntSumAggregator", 102 | "TemporalFloatMaxAggregator", 103 | "TemporalFloatMinAggregator", 104 | "TemporalFloatSumAggregator", 105 | "TemporalTextMaxAggregator", 106 | "TemporalTextMinAggregator", 107 | "TemporalPointExtentAggregator", 108 | "TimeInstantaneousUnionAggregator", 109 | "TimeContinuousUnionAggregator", 110 | # exceptions 111 | "MeosException", 112 | "MeosInternalError", 113 | "MeosArgumentError", 114 | "MeosIoError", 115 | "MeosInternalTypeError", 116 | "MeosValueOutOfRangeError", 117 | "MeosDivisionByZeroError", 118 | "MeosMemoryAllocError", 119 | "MeosAggregationError", 120 | "MeosDirectoryError", 121 | "MeosFileError", 122 | "MeosInvalidArgError", 123 | "MeosInvalidArgTypeError", 124 | "MeosInvalidArgValueError", 125 | "MeosMfJsonInputError", 126 | "MeosMfJsonOutputError", 127 | "MeosTextInputError", 128 | "MeosTextOutputError", 129 | "MeosWkbInputError", 130 | "MeosWkbOutputError", 131 | "MeosGeoJsonInputError", 132 | "MeosGeoJsonOutputError", 133 | ] 134 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test PyMEOS 2 | 3 | on: 4 | push: 5 | branches: [ "master", "stable-[0-9]+.[0-9]+" ] 6 | paths-ignore: 7 | - "docs/**" 8 | - ".readthedocs.yml" 9 | - "README.md" 10 | - ".github/ISSUE_TEMPLATE/**" 11 | pull_request: 12 | branches: [ "master", "stable-[0-9]+.[0-9]+" ] 13 | paths-ignore: 14 | - "docs/**" 15 | - ".readthedocs.yml" 16 | - "README.md" 17 | - ".github/ISSUE_TEMPLATE/**" 18 | 19 | jobs: 20 | test: 21 | name: Test PyMEOS - Python ${{ matrix.python-version }} on ${{ matrix.os }} 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] 27 | os: [ ubuntu-latest, macos-13, macos-14 ] 28 | include: 29 | - ld_path: "/usr/local/lib" 30 | - os: macos-14 31 | ld_path: "/opt/homebrew/lib" 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | 37 | - name: Get dependencies from apt (cache) 38 | uses: awalsh128/cache-apt-pkgs-action@latest 39 | if: runner.os == 'Linux' 40 | with: 41 | packages: build-essential cmake postgresql-server-dev-14 libproj-dev libjson-c-dev libgsl-dev libgeos-dev 42 | version: 1.0 43 | 44 | - name: Get dependencies from homebrew (cache) 45 | uses: tecolicom/actions-use-homebrew-tools@v1 46 | if: runner.os == 'macOS' 47 | with: 48 | tools: cmake libpq proj json-c gsl geos 49 | 50 | - name: Update brew 51 | if: matrix.os == 'macos-13' 52 | # Necessary to avoid issue with macOS runners. See 53 | # https://github.com/actions/runner-images/issues/4020 54 | run: | 55 | brew reinstall python@3.12 || brew link --overwrite python@3.12 56 | brew reinstall python@3.11 || brew link --overwrite python@3.11 57 | brew update 58 | 59 | - name: Fetch MEOS sources 60 | env: 61 | BRANCH_NAME: ${{ github.base_ref || github.ref_name }} 62 | run: | 63 | git clone --branch ${{ env.BRANCH_NAME }} --depth 1 https://github.com/MobilityDB/MobilityDB 64 | 65 | - name: Install MEOS 66 | run: | 67 | mkdir MobilityDB/build 68 | cd MobilityDB/build 69 | cmake .. -DMEOS=on 70 | make -j 71 | sudo make install 72 | 73 | - name: Set up Python ${{ matrix.python-version }} 74 | uses: actions/setup-python@v5 75 | with: 76 | python-version: ${{ matrix.python-version }} 77 | cache: "pip" 78 | 79 | - name: Fetch PyMEOS CFFI sources 80 | env: 81 | BRANCH_NAME: ${{ github.base_ref || github.ref_name }} 82 | run: | 83 | git clone --branch ${{ env.BRANCH_NAME }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI 84 | 85 | - name: Install python dependencies 86 | run: | 87 | python -m pip install --upgrade pip 88 | pip install -r PyMEOS-CFFI/dev-requirements.txt 89 | pip install -r dev-requirements.txt 90 | 91 | - name: Install pymeos_cffi 92 | run: | 93 | cd PyMEOS-CFFI 94 | python ./builder/build_header.py 95 | python ./builder/build_pymeos_functions.py 96 | pip install . 97 | 98 | - name: Test PyMEOS with pytest 99 | run: | 100 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{ matrix.ld_path }} 101 | export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:${{ matrix.ld_path }} 102 | pytest 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![MEOS Logo](https://raw.githubusercontent.com/MobilityDB/PyMEOS/master/docs/images/PyMEOS%20Logo.png) 2 | 3 | [![pypi](https://img.shields.io/pypi/v/pymeos.svg)](https://pypi.python.org/pypi/pymeos/) 4 | [![docs status](https://readthedocs.org/projects/pymeos/badge/?version=latest)](https://pymeos.readthedocs.io/en/latest/) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | 7 | [MEOS (Mobility Engine, Open Source)](https://www.libmeos.org/) is a C library which enables the manipulation of 8 | temporal and spatio-temporal data based on [MobilityDB](https://mobilitydb.com/)'s data types and functions. 9 | 10 | PyMEOS is a library built on top of MEOS that provides all of its functionality wrapped in a set of Python classes. 11 | 12 | For the PyMEOS CFFI library, the middle layer between MEOS and PyMEOS, see 13 | the [PyMEOS CFFI repository](https://github.com/MobilityDB/PyMEOS-CFFI). 14 | 15 | # Usage 16 | 17 | ## Installation 18 | 19 | You can install PyMEOS (`pymeos` and `pymeos-cffi`) using `pip`, `conda`, or from sources. 20 | 21 | ### Using pip 22 | 23 | ````shell 24 | pip install pymeos 25 | ```` 26 | 27 | > PyMEOS wheel should be compatible with any system, but it is possible that the pre-built distribution is 28 | > not available for PyMEOS CFFI for some OS/Architecture. 29 | 30 | ### Using conda 31 | 32 | PyMEOS is also available on the conda-forge channel. To install it, first add the conda-forge channel to your conda 33 | configuration: 34 | 35 | ````shell 36 | conda config --add channels conda-forge 37 | conda config --set channel_priority strict 38 | ```` 39 | 40 | Then, you can install PyMEOS using the following command: 41 | 42 | ````shell 43 | conda install conda-forge::pymeos 44 | ```` 45 | 46 | ### Source installation 47 | 48 | For detailed instructions on how to install PyMEOS from sources, see 49 | the [installation page](https://pymeos.readthedocs.io/en/latest/src/installation.html#) in the PyMEOS Documentation. 50 | 51 | ## Sample code 52 | 53 | > **IMPORTANT** Before using any PyMEOS function, always call `pymeos_initialize`. Otherwise, the library will 54 | > crash with a `Segmentation Fault` error. You should also always call `pymeos_finalize` at the end of your code. 55 | 56 | ````python 57 | from pymeos import pymeos_initialize, pymeos_finalize, TGeogPointInst, TGeogPointSeq 58 | 59 | # Important: Always initialize MEOS library 60 | pymeos_initialize() 61 | 62 | sequence_from_string = TGeogPointSeq( 63 | string='[Point(10.0 10.0)@2019-09-01 00:00:00+01, Point(20.0 20.0)@2019-09-02 00:00:00+01, Point(10.0 10.0)@2019-09-03 00:00:00+01]') 64 | print(f'Output: {sequence_from_string}') 65 | 66 | sequence_from_points = TGeogPointSeq(instant_list=[TGeogPointInst(string='Point(10.0 10.0)@2019-09-01 00:00:00+01'), 67 | TGeogPointInst(string='Point(20.0 20.0)@2019-09-02 00:00:00+01'), 68 | TGeogPointInst(string='Point(10.0 10.0)@2019-09-03 00:00:00+01')], 69 | lower_inc=True, upper_inc=True) 70 | speed = sequence_from_points.speed() 71 | print(f'Speeds: {speed}') 72 | 73 | # Call finish at the end of your code 74 | pymeos_finalize() 75 | ```` 76 | 77 | ```` 78 | Output: [POINT(10 10)@2019-09-01 01:00:00+02, POINT(20 20)@2019-09-02 01:00:00+02, POINT(10 10)@2019-09-03 01:00:00+02] 79 | Speeds: Interp=Step;[17.84556057812839@2019-09-01 01:00:00+02, 17.84556057812839@2019-09-03 01:00:00+02] 80 | ```` 81 | 82 | For more examples and use-cases, see [PyMEOS Examples repository](https://github.com/MobilityDB/PyMEOS-Examples) 83 | 84 | # Documentation 85 | 86 | Visit our [ReadTheDocs page](https://pymeos.readthedocs.io/en/latest/) for a more complete and detailed documentation, 87 | including an installation manual and several examples. -------------------------------------------------------------------------------- /pymeos/mixins/temporal_comparison.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TypeVar, TYPE_CHECKING 4 | 5 | from pymeos_cffi import ( 6 | teq_temporal_temporal, 7 | tne_temporal_temporal, 8 | tlt_temporal_temporal, 9 | tle_temporal_temporal, 10 | tgt_temporal_temporal, 11 | tge_temporal_temporal, 12 | ) 13 | 14 | Self = TypeVar("Self", bound="Temporal[Any]") 15 | 16 | if TYPE_CHECKING: 17 | from ..main import TBool 18 | from ..temporal import Temporal 19 | 20 | 21 | class TTemporallyEquatable: 22 | def temporal_equal(self: Self, other: Temporal) -> TBool: 23 | """ 24 | Returns the temporal equality relation between `self` and `other`. 25 | 26 | Args: 27 | other: A temporal object to compare to `self`. 28 | 29 | Returns: 30 | A :class:`TBool` with the result of the temporal equality relation. 31 | 32 | MEOS Functions: 33 | teq_temporal_temporal 34 | """ 35 | result = teq_temporal_temporal(self._inner, other._inner) 36 | return self._factory(result) 37 | 38 | def temporal_not_equal(self: Self, other: Temporal) -> TBool: 39 | """ 40 | Returns the temporal not equal relation between `self` and `other`. 41 | 42 | Args: 43 | other: A temporal object to compare to `self`. 44 | 45 | Returns: 46 | A :class:`TBool` with the result of the temporal not equal relation. 47 | 48 | MEOS Functions: 49 | tne_temporal_temporal 50 | """ 51 | result = tne_temporal_temporal(self._inner, other._inner) 52 | return self._factory(result) 53 | 54 | 55 | class TTemporallyComparable: 56 | def temporal_less(self: Self, other: Temporal) -> TBool: 57 | """ 58 | Returns the temporal less than relation between `self` and `other`. 59 | 60 | Args: 61 | other: A temporal object to compare to `self`. 62 | 63 | Returns: 64 | A :class:`TBool` with the result of the temporal less than relation. 65 | 66 | MEOS Functions: 67 | tlt_temporal_temporal 68 | """ 69 | result = tlt_temporal_temporal(self._inner, other._inner) 70 | return self._factory(result) 71 | 72 | def temporal_less_or_equal(self: Self, other: Temporal) -> TBool: 73 | """ 74 | Returns the temporal less or equal relation between `self` and `other`. 75 | 76 | Args: 77 | other: A temporal object to compare to `self`. 78 | 79 | Returns: 80 | A :class:`TBool` with the result of the temporal less or equal 81 | relation. 82 | 83 | MEOS Functions: 84 | tle_temporal_temporal 85 | """ 86 | result = tle_temporal_temporal(self._inner, other._inner) 87 | return self._factory(result) 88 | 89 | def temporal_greater_or_equal(self: Self, other: Temporal) -> TBool: 90 | """ 91 | Returns the temporal greater or equal relation between `self` and 92 | `other`. 93 | 94 | Args: 95 | other: A temporal object to compare to `self`. 96 | 97 | Returns: 98 | A :class:`TBool` with the result of the temporal greater or equal 99 | relation. 100 | 101 | MEOS Functions: 102 | tge_temporal_temporal 103 | """ 104 | result = tge_temporal_temporal(self._inner, other._inner) 105 | return self._factory(result) 106 | 107 | def temporal_greater(self: Self, other: Temporal) -> TBool: 108 | """ 109 | Returns the temporal greater than relation between `self` and `other`. 110 | 111 | Args: 112 | other: A temporal object to compare to `self`. 113 | 114 | Returns: 115 | A :class:`TBool` with the result of the temporal greater than 116 | relation. 117 | 118 | MEOS Functions: 119 | tgt_temporal_temporal 120 | """ 121 | result = tgt_temporal_temporal(self._inner, other._inner) 122 | return self._factory(result) 123 | -------------------------------------------------------------------------------- /pymeos/plotters/point_sequence_plotter.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | from ..main import TPointSeq, TPointInst 6 | from ..temporal import TInterpolation 7 | 8 | 9 | class TemporalPointSequencePlotter: 10 | """ 11 | Plotter for :class:`TPointSeq` and lists of :class:`TPointInst`. 12 | """ 13 | 14 | @staticmethod 15 | def plot_xy( 16 | sequence: Union[TPointSeq, List[TPointInst]], 17 | *args, 18 | axes=None, 19 | show_markers=True, 20 | show_grid=True, 21 | **kwargs, 22 | ): 23 | """ 24 | Plot a TPointSeq or a list of TPointInst on the given axes. The actual plot function is chosen 25 | based on the interpolation of the sequence. 26 | 27 | Params: 28 | sequence: The :class:`TPointSeq` or list of :class:`TPointInst` to plot. 29 | axes: The axes to plot on. If None, the current axes are used. 30 | show_markers: Whether to show markers at the start and end of the sequence. The marker will be filled if the 31 | sequence is inclusive at that end, and empty otherwise. 32 | show_grid: Whether to show a grid. 33 | *args: Additional arguments to pass to the plot function. 34 | **kwargs: Additional keyword arguments to pass to the plot function. 35 | 36 | Returns: 37 | List with the plotted elements. 38 | """ 39 | base = axes or plt.gca() 40 | linear = False 41 | if isinstance(sequence, list): 42 | plot_func = base.scatter 43 | elif sequence.interpolation() == TInterpolation.LINEAR: 44 | plot_func = base.plot 45 | linear = True 46 | else: 47 | plot_func = base.scatter 48 | 49 | ins: list[TPointInst] = ( 50 | sequence.instants() if isinstance(sequence, TPointSeq) else sequence 51 | ) 52 | x = [i.x().value() for i in ins] 53 | y = [i.y().value() for i in ins] 54 | 55 | base.set_axisbelow(True) 56 | 57 | if show_grid: 58 | base.grid(zorder=0.5) 59 | plots = [plot_func(x, y, *args, **kwargs)] 60 | 61 | if linear and show_markers: 62 | color = plots[0][0].get_color() 63 | plots.append( 64 | base.scatter( 65 | x[0], 66 | y[0], 67 | s=80, 68 | marker="o", 69 | facecolors=color if sequence.lower_inc() else "none", 70 | edgecolors=color, 71 | zorder=2 if sequence.lower_inc() else 3, 72 | ) 73 | ) 74 | plots.append( 75 | base.scatter( 76 | x[-1], 77 | y[-1], 78 | s=80, 79 | marker="o", 80 | facecolors=color if sequence.upper_inc() else "none", 81 | edgecolors=color, 82 | zorder=2 if sequence.upper_inc() else 3, 83 | ) 84 | ) 85 | 86 | base.tick_params(axis="x", rotation=45) 87 | 88 | return plots 89 | 90 | @staticmethod 91 | def plot_sequences_xy(sequences: List[TPointSeq], *args, **kwargs): 92 | """ 93 | Plot a list of TPointSeq on the given axes. Every sequence will be plotted in a different color. 94 | 95 | Params: 96 | sequences: The list of :class:`TPointSeq` to plot. 97 | *args: Additional arguments to pass to the plot function. 98 | **kwargs: Additional keyword arguments to pass to the plot function. 99 | 100 | Returns: 101 | List of lists with the plotted elements of each sequence. 102 | 103 | See Also: 104 | :func:`~pymeos.plotters.point_sequence_plotter.TemporalPointSequencePlotter.plot_xy`, 105 | :meth:`~pymeos.plotters.point_sequenceset_plotter.TemporalPointSequenceSetPlotter.plot_xy` 106 | """ 107 | plots = [] 108 | for seq in sequences: 109 | plots.append(TemporalPointSequencePlotter.plot_xy(seq, *args, **kwargs)) 110 | return plots 111 | -------------------------------------------------------------------------------- /pymeos/plotters/time_plotter.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Union 3 | 4 | from matplotlib import pyplot as plt 5 | 6 | from ..collections import TsTzSet, TsTzSpan, TsTzSpanSet 7 | from ..collections.time.dateset import DateSet 8 | 9 | 10 | class TimePlotter: 11 | """ 12 | Plotter for :class:`Time` objects. 13 | """ 14 | 15 | @staticmethod 16 | def plot_timestamp(timestamp: datetime, *args, axes=None, **kwargs): 17 | """ 18 | Plot a :class:`datetime` on the given axes as a vertical line. 19 | 20 | Params: 21 | timestamp: The :class:`datetime` to plot. 22 | axes: The axes to plot on. If None, the current axes are used. 23 | *args: Additional arguments to pass to the plot function. 24 | **kwargs: Additional keyword arguments to pass to the plot function. 25 | 26 | Returns: 27 | List with the plotted elements. 28 | """ 29 | base = axes or plt.gca() 30 | return base.axvline(timestamp, *args, **kwargs) 31 | 32 | @staticmethod 33 | def plot_tstzset(tstzset: TsTzSet, *args, axes=None, **kwargs): 34 | """ 35 | Plot a :class:`TsTzSet` on the given axes as a vertical line for each timestamp. 36 | 37 | Params: 38 | tstzset: The :class:`TsTzSet` to plot. 39 | axes: The axes to plot on. If None, the current axes are used. 40 | *args: Additional arguments to pass to the plot function. 41 | **kwargs: Additional keyword arguments to pass to the plot function. 42 | 43 | Returns: 44 | List with the plotted elements. 45 | """ 46 | base = axes or plt.gca() 47 | stamps = tstzset.elements() 48 | plot = base.axvline(stamps[0], *args, **kwargs) 49 | kwargs.pop("label", None) 50 | plots = [plot] 51 | for stamp in stamps[1:]: 52 | plots.append(base.axvline(stamp, *args, color=plot.get_color(), **kwargs)) 53 | return plots 54 | 55 | @staticmethod 56 | def plot_tstzspan(tstzspan: TsTzSpan, *args, axes=None, **kwargs): 57 | """ 58 | Plot a :class:`TsTzSpan` on the given axes as two vertical lines filled in between. The lines will be 59 | dashed if the tstzspan is open. 60 | 61 | Params: 62 | tstzspan: The :class:`TsTzSpan` to plot. 63 | axes: The axes to plot on. If None, the current axes are used. 64 | *args: Additional arguments to pass to the plot function. 65 | **kwargs: Additional keyword arguments to pass to the plot function. 66 | 67 | Returns: 68 | List with the plotted elements. 69 | """ 70 | base = axes or plt.gca() 71 | ll = base.axvline( 72 | tstzspan.lower(), 73 | *args, 74 | linestyle="-" if tstzspan.lower_inc() else "--", 75 | **kwargs, 76 | ) 77 | kwargs.pop("label", None) 78 | ul = base.axvline( 79 | tstzspan.upper(), 80 | *args, 81 | linestyle="-" if tstzspan.upper_inc() else "--", 82 | **kwargs, 83 | ) 84 | s = base.axvspan(tstzspan.lower(), tstzspan.upper(), *args, alpha=0.3, **kwargs) 85 | return [ll, ul, s] 86 | 87 | @staticmethod 88 | def plot_tstzspanset(tstzspanset: TsTzSpanSet, *args, axes=None, **kwargs): 89 | """ 90 | Plot a :class:`TsTzSpanSet` on the given axes as a vertical line for each timestamp. 91 | 92 | Params: 93 | tstzspanset: The :class:`TsTzSpanSet` to plot. 94 | *args: Additional arguments to pass to the plot function. 95 | axes: The axes to plot on. If None, the current axes are used. 96 | 97 | Returns: 98 | List with the plotted elements. 99 | 100 | See also: 101 | :func:`~pymeos.plotters.time_plotter.TimePlotter.plot_tstzspan` 102 | """ 103 | tstzspans = tstzspanset.tstzspans() 104 | line = TimePlotter.plot_tstzspan(tstzspans[0], *args, axes=axes, **kwargs) 105 | kwargs.pop("label", None) 106 | lines = [line] 107 | if "color" not in kwargs: 108 | kwargs["color"] = line[0].get_color() 109 | for p in tstzspans[1:]: 110 | lines.append(TimePlotter.plot_tstzspan(p, *args, axes=axes, **kwargs)) 111 | return lines 112 | -------------------------------------------------------------------------------- /docs/src/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Built distributions 5 | ------------------- 6 | 7 | Built distributions don't require compiling PyMEOS, PyMEOS CFFI or MEOS, 8 | and can be installed using ``pip``. 9 | 10 | Installation from PyPI 11 | ^^^^^^^^^^^^^^^^^^^^^^ 12 | 13 | PyMEOS and PyMEOS CFFI are available as binary distributions (wheel) for Linux (x64) and MacOS (x64 and arm64) platforms 14 | on `PyPI `__. The distributions include the most recent version of MEOS available at 15 | the time of the PyMEOS release. Install the binary wheel with pip as follows: 16 | 17 | .. code-block:: console 18 | 19 | pip install pymeos 20 | 21 | 22 | Installation using conda 23 | ^^^^^^^^^^^^^^^^^^^^^^^^ 24 | 25 | PyMEOS is also available on the conda-forge channel. 26 | First, set conda-forge as the priority channel: 27 | 28 | .. code-block:: console 29 | 30 | conda config --add channels conda-forge 31 | conda config --set channel_priority strict 32 | 33 | Then, install PyMEOS as follows: 34 | 35 | .. code-block:: console 36 | 37 | conda install conda-forge::pymeos 38 | 39 | 40 | Installation from source with custom MEOS library 41 | ------------------------------------------------- 42 | 43 | If you want to use a specific MEOS version or a MEOS distribution that is 44 | already present on your system, you can compile the PyMEOS packages from source yourself, 45 | by directing pip to ignore the binary wheels. 46 | 47 | Note that only PyMEOS CFFI will need to be compiled from sources, 48 | since PyMEOS is a pure Python package and doesn't interact with MEOS directly. 49 | 50 | First, make sure that MEOS is installed in your system. You can install it following the instructions 51 | in the `MEOS documentation `__. 52 | 53 | Then, install PyMEOS CFFI from source: 54 | 55 | .. code-block:: console 56 | 57 | pip install pymeos-cffi --no-binary pymeos-cffi 58 | pip install pymeos 59 | 60 | 61 | Updating the PyMEOS CFFI wrapper for custom MEOS library 62 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 63 | 64 | If your MEOS library API doesn't match the one used by the PyMEOS CFFI wrapper, it will crash. You can fix this 65 | by updating the header file used by PyMEOS CFFI to match your MEOS version. To do so, you will need to recompile it 66 | using the builder scripts provided in the ``pymeos_cffi`` package. 67 | 68 | .. warning:: 69 | While you can easily update ``pymeos_cffi``, you won't be able to do it so easily 70 | with ``pymeos``. If you want to use the ``pymeos`` library with your custom 71 | ``pymeos_cffi``, you should make sure that the part of the API used by ``pymeos`` 72 | hasn't changed, or you'll get an import error when using ``pymeos``. 73 | 74 | First, you will need to get the source code of PyMEOS CFFI. You can do so by downloading the source distribution 75 | from PyPI, or by cloning the repository from GitHub: 76 | 77 | .. code-block:: console 78 | 79 | git clone git@github.com:MobilityDB/PyMEOS.git 80 | cd PyMEOS/pymeos_cffi 81 | 82 | Then, you will need to run the header builder script, which will generate a new header file based on the MEOS 83 | version installed in your system. The script accepts two parameters, a path to the MEOS header file, and a path to your 84 | MEOS library: 85 | 86 | .. code-block:: console 87 | 88 | python3 ./pymeos_cffi/builder/build_header.py 89 | 90 | If no parameters are passed, the script will use the default header file and library path: 91 | 92 | .. code-block:: console 93 | 94 | python3 ./pymeos_cffi/builder/build_header.py /usr/local/include/meos.h /usr/local/lib/libmeos.so 95 | 96 | The second parameter is optional and is used to remove any function defined in the header file not exposed by the 97 | library. If omitted, this step will not be performed. 98 | 99 | Then, you have to generate the PyMEOS CFFI wrapper functions using the functions builder script: 100 | 101 | .. code-block:: console 102 | 103 | python3 ./pymeos_cffi/builder/build_pymeos_functions.py 104 | 105 | This will update the ``functions.py`` file that contains all the python functions exposed by the library. 106 | 107 | Finally, you can install the updated PyMEOS CFFI package: 108 | 109 | .. code-block:: console 110 | 111 | pip install . 112 | 113 | -------------------------------------------------------------------------------- /pymeos/temporal/tsequence.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from typing import Optional, Union, List, Any, TypeVar, Type 5 | 6 | from pymeos_cffi import * 7 | 8 | from .interpolation import TInterpolation 9 | from .temporal import Temporal 10 | 11 | TBase = TypeVar("TBase") 12 | TG = TypeVar("TG", bound="Temporal[Any]") 13 | TI = TypeVar("TI", bound="TInstant[Any]") 14 | TS = TypeVar("TS", bound="TSequence[Any]") 15 | TSS = TypeVar("TSS", bound="TSequenceSet[Any]") 16 | Self = TypeVar("Self", bound="TSequence[Any]") 17 | 18 | 19 | class TSequence(Temporal[TBase, TG, TI, TS, TSS], ABC): 20 | """ 21 | Base class for temporal sequence types, i.e. temporal values that are 22 | defined over a continuous tstzspan of time. 23 | """ 24 | 25 | # ------------------------- Constructors ---------------------------------- 26 | def __init__( 27 | self, 28 | string: Optional[str] = None, 29 | *, 30 | instant_list: Optional[List[Union[str, Any]]] = None, 31 | lower_inc: bool = True, 32 | upper_inc: bool = False, 33 | interpolation: TInterpolation = TInterpolation.LINEAR, 34 | normalize: bool = True, 35 | _inner=None, 36 | ): 37 | assert (_inner is not None) or ( 38 | (string is not None) != (instant_list is not None) 39 | ), "Either string must be not None or instant_list must be not" 40 | if _inner is not None: 41 | self._inner = as_tsequence(_inner) 42 | elif string is not None: 43 | self._inner = as_tsequence(self.__class__._parse_function(string)) 44 | else: 45 | self._instants = [ 46 | ( 47 | x._inner 48 | if isinstance(x, self.ComponentClass) 49 | else self.__class__._parse_function(x) 50 | ) 51 | for x in instant_list 52 | ] 53 | count = len(self._instants) 54 | self._inner = tsequence_make( 55 | self._instants, 56 | count, 57 | lower_inc, 58 | upper_inc, 59 | interpolation.value, 60 | normalize, 61 | ) 62 | 63 | @classmethod 64 | def from_instants( 65 | cls: Type[Self], 66 | instant_list: Optional[List[Union[str, Any]]], 67 | lower_inc: bool = True, 68 | upper_inc: bool = False, 69 | interpolation: TInterpolation = TInterpolation.LINEAR, 70 | normalize: bool = True, 71 | ) -> Self: 72 | """ 73 | Create a temporal sequence from a list of instants. 74 | 75 | Args: 76 | instant_list: List of instants 77 | lower_inc: Whether the lower bound is inclusive 78 | upper_inc: Whether the upper bound is inclusive 79 | interpolation: Interpolation method 80 | normalize: Whether to normalize the sequence 81 | 82 | Returns: 83 | A new temporal sequence. 84 | """ 85 | return cls( 86 | instant_list=instant_list, 87 | lower_inc=lower_inc, 88 | upper_inc=upper_inc, 89 | interpolation=interpolation, 90 | normalize=normalize, 91 | ) 92 | 93 | # ------------------------- Accessors ------------------------------------- 94 | def lower_inc(self) -> bool: 95 | """ 96 | Returns whether the lower bound is inclusive. 97 | 98 | Returns: 99 | `True` if the lower bound is inclusive, `False` otherwise. 100 | """ 101 | return self._inner.period.lower_inc 102 | 103 | def upper_inc(self) -> bool: 104 | """ 105 | Returns whether the upper bound is inclusive. 106 | 107 | Returns: 108 | `True` if the upper bound is inclusive, `False` otherwise. 109 | """ 110 | return self._inner.period.upper_inc 111 | 112 | # ------------------------- Plot Operations ------------------------------- 113 | def plot(self, *args, **kwargs): 114 | """ 115 | Plot the temporal sequence. 116 | 117 | See Also: 118 | :meth:`pymeos.plotters.TemporalSequencePlotter.plot` 119 | """ 120 | from ..plotters import TemporalSequencePlotter 121 | 122 | return TemporalSequencePlotter.plot(self, *args, **kwargs) 123 | -------------------------------------------------------------------------------- /pymeos/plotters/sequence_plotter.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Union, List 3 | 4 | import matplotlib.pyplot as plt 5 | 6 | from ..temporal import TSequence, TInterpolation, TInstant 7 | 8 | 9 | class TemporalSequencePlotter: 10 | """ 11 | Plotter for :class:`TSequence` and lists of :class:`TInstant`. 12 | """ 13 | 14 | @staticmethod 15 | def plot( 16 | sequence: Union[TSequence, List[TInstant]], 17 | *args, 18 | axes=None, 19 | show_markers=True, 20 | show_grid=True, 21 | **kwargs, 22 | ): 23 | """ 24 | Plot a :class:`TSequence` or a list of :class:`TInstant` on the given axes. The actual plot function is chosen 25 | based on the interpolation of the sequence. 26 | 27 | Params: 28 | sequence: The :class:`TSequence` or list of :class:`TInstant` to plot. 29 | axes: The axes to plot on. If None, the current axes are used. 30 | show_markers: Whether to show markers at the start and end of the sequence. The marker will be filled if the 31 | sequence is inclusive at that end, and empty otherwise. 32 | show_grid: Whether to show a grid. 33 | *args: Additional arguments to pass to the plot function. 34 | **kwargs: Additional keyword arguments to pass to the plot function. 35 | 36 | Returns: 37 | List with the plotted elements. 38 | """ 39 | base = axes or plt.gca() 40 | if isinstance(sequence, list): 41 | plot_func = base.scatter 42 | show_markers = False 43 | elif sequence.interpolation() == TInterpolation.LINEAR: 44 | plot_func = base.plot 45 | elif sequence.interpolation() == TInterpolation.STEPWISE: 46 | plot_func = partial(base.step, where="post") 47 | else: 48 | plot_func = base.scatter 49 | show_markers = False 50 | 51 | ins = sequence.instants() if isinstance(sequence, TSequence) else sequence 52 | x = [i.timestamp() for i in ins] 53 | y = [i.value() for i in ins] 54 | 55 | base.set_axisbelow(True) 56 | 57 | if show_grid: 58 | base.grid(zorder=0.5) 59 | plots = [plot_func(x, y, *args, **kwargs)] 60 | 61 | if show_markers: 62 | color = plots[0][0].get_color() 63 | plots.append( 64 | base.scatter( 65 | x[0], 66 | y[0], 67 | s=40, 68 | marker="o", 69 | facecolors=color if sequence.lower_inc() else "none", 70 | edgecolors=color, 71 | zorder=2 if sequence.lower_inc() else 3, 72 | ) 73 | ) 74 | plots.append( 75 | base.scatter( 76 | x[-1], 77 | y[-1], 78 | s=40, 79 | marker="o", 80 | facecolors=color if sequence.upper_inc() else "none", 81 | edgecolors=color, 82 | zorder=2 if sequence.upper_inc() else 3, 83 | ) 84 | ) 85 | 86 | if isinstance(y[0], bool): 87 | plt.yticks([1.0, 0.0], ["True", "False"]) 88 | plt.ylim(-0.25, 1.25) 89 | 90 | base.tick_params(axis="x", rotation=45) 91 | 92 | return plots 93 | 94 | @staticmethod 95 | def plot_sequences(sequences: List[TSequence], *args, **kwargs): 96 | """ 97 | Plot a list of :class:`TSequence` on the given axes. Every sequence will be plotted in a different color. 98 | 99 | Params: 100 | sequences: The list of :class:`TSequence` to plot. 101 | *args: Additional arguments to pass to the plot function. 102 | **kwargs: Additional keyword arguments to pass to the plot function. 103 | 104 | Returns: 105 | List of lists with the plotted elements of each sequence. 106 | 107 | See Also: 108 | :func:`~pymeos.plotters.sequence_plotter.TemporalSequencePlotter.plot`, 109 | :meth:`~pymeos.plotters.sequenceset_plotter.TemporalSequenceSetPlotter.plot` 110 | """ 111 | plots = [] 112 | for seq in sequences: 113 | plots.append(TemporalSequencePlotter.plot(seq, *args, **kwargs)) 114 | return plots 115 | -------------------------------------------------------------------------------- /pymeos/aggregators/general_aggregators.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Union 3 | 4 | from pymeos_cffi import ( 5 | timestamptz_tcount_transfn, 6 | datetime_to_timestamptz, 7 | tstzset_tcount_transfn, 8 | temporal_tcount_transfn, 9 | tstzspan_tcount_transfn, 10 | tstzspanset_tcount_transfn, 11 | temporal_extent_transfn, 12 | timestamptz_extent_transfn, 13 | set_extent_transfn, 14 | span_extent_transfn, 15 | spanset_extent_transfn, 16 | ) 17 | 18 | from .aggregator import BaseAggregator 19 | from ..boxes import Box 20 | from ..collections import Time, TsTzSet, TsTzSpan, TsTzSpanSet 21 | from ..main import TIntSeq, TIntSeqSet 22 | from ..temporal import Temporal, TInterpolation 23 | 24 | 25 | class TemporalInstantCountAggregator( 26 | BaseAggregator[Union[datetime, TsTzSet, Temporal], TIntSeq] 27 | ): 28 | """ 29 | Temporal count for instantaneous temporal objects: 30 | 31 | - :class:`~datetime.datetime` 32 | - :class:`~pymeos.time.tstzset.TsTzSet` 33 | - :class:`~pymeos.temporal.temporal.Temporal` with Discrete interpolation 34 | 35 | MEOS Functions: 36 | timestamp_tcount_transfn, tstzset_tcount_transfn, temporal_tcount_transfn, temporal_tagg_finalfn 37 | """ 38 | 39 | @classmethod 40 | def _add(cls, state, temporal): 41 | if isinstance(temporal, datetime): 42 | state = timestamptz_tcount_transfn(state, datetime_to_timestamptz(temporal)) 43 | elif isinstance(temporal, TsTzSet): 44 | state = tstzset_tcount_transfn(state, temporal._inner) 45 | elif ( 46 | isinstance(temporal, Temporal) 47 | and temporal.interpolation() == TInterpolation.DISCRETE 48 | ): 49 | state = temporal_tcount_transfn(state, temporal._inner) 50 | else: 51 | cls._error(temporal) 52 | return state 53 | 54 | 55 | class TemporalPeriodCountAggregator( 56 | BaseAggregator[Union[TsTzSpan, TsTzSpanSet, Temporal], TIntSeqSet] 57 | ): 58 | """ 59 | Temporal count for non-instantaneous temporal objects: 60 | 61 | - :class:`~pymeos.time.tstzspan.TsTzSpan` 62 | - :class:`~pymeos.time.tstzspanset.TsTzSpanSet` 63 | - :class:`~pymeos.temporal.temporal.Temporal` without Discrete interpolation 64 | 65 | MEOS Functions: 66 | tstzspan_tcount_transfn, tstzspanset_tcount_transfn, temporal_tcount_transfn, temporal_tagg_finalfn 67 | """ 68 | 69 | @classmethod 70 | def _add(cls, state, temporal): 71 | if isinstance(temporal, TsTzSpan): 72 | state = tstzspan_tcount_transfn(state, temporal._inner) 73 | elif isinstance(temporal, TsTzSpanSet): 74 | state = tstzspanset_tcount_transfn(state, temporal._inner) 75 | elif ( 76 | isinstance(temporal, Temporal) 77 | and temporal.interpolation() != TInterpolation.DISCRETE 78 | ): 79 | state = temporal_tcount_transfn(state, temporal._inner) 80 | else: 81 | cls._error(temporal) 82 | return state 83 | 84 | 85 | class TemporalExtentAggregator(BaseAggregator[Union[Time, Temporal], TsTzSpan]): 86 | """ 87 | Temporal extent of any kind of temporal object, i.e. smallest :class:`~pymeos.time.tstzspan.TsTzSpan` that includes 88 | all aggregated temporal objects. 89 | 90 | MEOS Functions: 91 | temporal_extent_transfn, timestamp_extent_transfn, tstzset_extent_transfn, span_extent_transfn, 92 | spanset_extent_transfn, temporal_tagg_finalfn 93 | """ 94 | 95 | @classmethod 96 | def _add(cls, state, temporal): 97 | if isinstance(temporal, Temporal): 98 | state = temporal_extent_transfn(state, temporal._inner) 99 | elif isinstance(temporal, datetime): 100 | state = timestamptz_extent_transfn(state, datetime_to_timestamptz(temporal)) 101 | elif isinstance(temporal, TsTzSet): 102 | state = set_extent_transfn(state, temporal._inner) 103 | elif isinstance(temporal, TsTzSpan): 104 | state = span_extent_transfn(state, temporal._inner) 105 | pass 106 | elif isinstance(temporal, TsTzSpanSet): 107 | state = spanset_extent_transfn(state, temporal._inner) 108 | else: 109 | cls._error(temporal) 110 | return state 111 | 112 | @classmethod 113 | def _finish(cls, state) -> Union[Temporal, Time, Box]: 114 | return TsTzSpan(_inner=state) 115 | -------------------------------------------------------------------------------- /pymeos/factory.py: -------------------------------------------------------------------------------- 1 | from pymeos_cffi import MeosType, MeosTemporalSubtype 2 | 3 | from .main import ( 4 | TBoolInst, 5 | TBoolSeq, 6 | TBoolSeqSet, 7 | TIntInst, 8 | TIntSeq, 9 | TIntSeqSet, 10 | TFloatInst, 11 | TFloatSeq, 12 | TFloatSeqSet, 13 | TTextInst, 14 | TTextSeq, 15 | TTextSeqSet, 16 | TGeomPointInst, 17 | TGeomPointSeq, 18 | TGeomPointSeqSet, 19 | TGeogPointInst, 20 | TGeogPointSeq, 21 | TGeogPointSeqSet, 22 | ) 23 | from .collections import ( 24 | GeometrySet, 25 | GeographySet, 26 | IntSet, 27 | IntSpan, 28 | IntSpanSet, 29 | FloatSet, 30 | FloatSpan, 31 | FloatSpanSet, 32 | TextSet, 33 | DateSet, 34 | DateSpan, 35 | DateSpanSet, 36 | TsTzSet, 37 | TsTzSpan, 38 | TsTzSpanSet, 39 | ) 40 | 41 | 42 | class _TemporalFactory: 43 | """ 44 | Factory class to create the proper PyMEOS class from a MEOS object. 45 | 46 | This class is used internally by PyMEOS classes and there shouldn't be any need to 47 | be used outside of them. 48 | """ 49 | 50 | _mapper = { 51 | (MeosType.T_TBOOL, MeosTemporalSubtype.INSTANT): TBoolInst, 52 | (MeosType.T_TBOOL, MeosTemporalSubtype.SEQUENCE): TBoolSeq, 53 | (MeosType.T_TBOOL, MeosTemporalSubtype.SEQUENCE_SET): TBoolSeqSet, 54 | (MeosType.T_TINT, MeosTemporalSubtype.INSTANT): TIntInst, 55 | (MeosType.T_TINT, MeosTemporalSubtype.SEQUENCE): TIntSeq, 56 | (MeosType.T_TINT, MeosTemporalSubtype.SEQUENCE_SET): TIntSeqSet, 57 | (MeosType.T_TFLOAT, MeosTemporalSubtype.INSTANT): TFloatInst, 58 | (MeosType.T_TFLOAT, MeosTemporalSubtype.SEQUENCE): TFloatSeq, 59 | (MeosType.T_TFLOAT, MeosTemporalSubtype.SEQUENCE_SET): TFloatSeqSet, 60 | (MeosType.T_TTEXT, MeosTemporalSubtype.INSTANT): TTextInst, 61 | (MeosType.T_TTEXT, MeosTemporalSubtype.SEQUENCE): TTextSeq, 62 | (MeosType.T_TTEXT, MeosTemporalSubtype.SEQUENCE_SET): TTextSeqSet, 63 | (MeosType.T_TGEOMPOINT, MeosTemporalSubtype.INSTANT): TGeomPointInst, 64 | (MeosType.T_TGEOMPOINT, MeosTemporalSubtype.SEQUENCE): TGeomPointSeq, 65 | (MeosType.T_TGEOMPOINT, MeosTemporalSubtype.SEQUENCE_SET): TGeomPointSeqSet, 66 | (MeosType.T_TGEOGPOINT, MeosTemporalSubtype.INSTANT): TGeogPointInst, 67 | (MeosType.T_TGEOGPOINT, MeosTemporalSubtype.SEQUENCE): TGeogPointSeq, 68 | (MeosType.T_TGEOGPOINT, MeosTemporalSubtype.SEQUENCE_SET): TGeogPointSeqSet, 69 | } 70 | 71 | @staticmethod 72 | def create_temporal(inner): 73 | """ 74 | Creates the appropriate PyMEOS Temporal class from a meos object. 75 | 76 | Args: 77 | inner: MEOS object. 78 | 79 | Returns: 80 | An instance of the appropriate subclass of :class:`Temporal` wrapping 81 | `inner`. 82 | """ 83 | if inner is None: 84 | return None 85 | temp_type = (inner.temptype, inner.subtype) 86 | return _TemporalFactory._mapper[temp_type](_inner=inner) 87 | 88 | 89 | class _CollectionFactory: 90 | """ 91 | Factory class to create the proper PyMEOS collection class from a MEOS object. 92 | 93 | This class is used internally by PyMEOS classes and there shouldn't be any need 94 | to be used outside of them. 95 | """ 96 | 97 | _mapper = { 98 | MeosType.T_GEOMSET: GeometrySet, 99 | MeosType.T_GEOGSET: GeographySet, 100 | MeosType.T_INTSET: IntSet, 101 | MeosType.T_INTSPAN: IntSpan, 102 | MeosType.T_INTSPANSET: IntSpanSet, 103 | MeosType.T_FLOATSET: FloatSet, 104 | MeosType.T_FLOATSPAN: FloatSpan, 105 | MeosType.T_FLOATSPANSET: FloatSpanSet, 106 | MeosType.T_TEXTSET: TextSet, 107 | MeosType.T_DATESET: DateSet, 108 | MeosType.T_DATESPAN: DateSpan, 109 | MeosType.T_DATESPANSET: DateSpanSet, 110 | MeosType.T_TSTZSET: TsTzSet, 111 | MeosType.T_TSTZSPAN: TsTzSpan, 112 | MeosType.T_TSTZSPANSET: TsTzSpanSet, 113 | } 114 | 115 | @staticmethod 116 | def create_collection(inner): 117 | """ 118 | Creates the appropriate PyMEOS Collection class from a meos object. 119 | 120 | Args: 121 | inner: MEOS object. 122 | 123 | Returns: 124 | An instance of the appropriate subclass of :class:`Collection` wrapping 125 | `inner`. 126 | """ 127 | if inner is None: 128 | return None 129 | 130 | attributes = ["spansettype", "spantype", "settype"] 131 | collection_type = next( 132 | getattr(inner, attribute) 133 | for attribute in attributes 134 | if hasattr(inner, attribute) 135 | ) 136 | return _CollectionFactory._mapper[collection_type](_inner=inner) 137 | -------------------------------------------------------------------------------- /tests/collections/text/textset_test.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from datetime import datetime, timezone, timedelta 3 | from typing import List 4 | 5 | import pytest 6 | 7 | from pymeos import TextSet 8 | from tests.conftest import TestPyMEOS 9 | 10 | 11 | class TestTextSet(TestPyMEOS): 12 | tset = TextSet("{A, BB, ccc}") 13 | 14 | @staticmethod 15 | def assert_textset_equality(tset: TextSet, elements: List[str]): 16 | assert tset.num_elements() == len(elements) 17 | assert tset.elements() == elements 18 | 19 | 20 | class TestTextSetConstructors(TestTextSet): 21 | def test_string_constructor(self): 22 | self.assert_textset_equality(self.tset, ["A", "BB", "ccc"]) 23 | 24 | def test_list_constructor(self): 25 | ts_set = TextSet(elements=["A", "BB", "ccc"]) 26 | self.assert_textset_equality(ts_set, ["A", "BB", "ccc"]) 27 | 28 | def test_hexwkb_constructor(self): 29 | ts_set = TextSet.from_hexwkb(TextSet(elements=["A", "BB", "ccc"]).as_hexwkb()) 30 | self.assert_textset_equality(ts_set, ["A", "BB", "ccc"]) 31 | 32 | def test_from_as_constructor(self): 33 | assert self.tset == TextSet(str(self.tset)) 34 | assert self.tset == TextSet.from_wkb(self.tset.as_wkb()) 35 | assert self.tset == TextSet.from_hexwkb(self.tset.as_hexwkb()) 36 | 37 | def test_copy_constructor(self): 38 | ts_set_copy = copy(self.tset) 39 | assert self.tset == ts_set_copy 40 | assert self.tset is not ts_set_copy 41 | 42 | 43 | class TestTextSetOutputs(TestTextSet): 44 | def test_str(self): 45 | assert str(self.tset) == '{"A", "BB", "ccc"}' 46 | 47 | def test_repr(self): 48 | assert repr(self.tset) == 'TextSet({"A", "BB", "ccc"})' 49 | 50 | def test_as_hexwkb(self): 51 | assert self.tset == TextSet.from_hexwkb(self.tset.as_hexwkb()) 52 | 53 | 54 | class TestTimestampConversions(TestTextSet): 55 | def test_to_spanset(self): 56 | with pytest.raises(NotImplementedError): 57 | self.tset.to_spanset() 58 | 59 | def test_to_span(self): 60 | with pytest.raises(NotImplementedError): 61 | self.tset.to_span() 62 | 63 | 64 | class TestTextSetAccessors(TestTextSet): 65 | def test_num_elements(self): 66 | assert self.tset.num_elements() == 3 67 | assert len(self.tset) == 3 68 | 69 | def test_start_element(self): 70 | assert self.tset.start_element() == "A" 71 | 72 | def test_end_element(self): 73 | assert self.tset.end_element() == "ccc" 74 | 75 | def test_element_n(self): 76 | assert self.tset.element_n(1) == "BB" 77 | 78 | def test_element_n_out_of_range(self): 79 | with pytest.raises(IndexError): 80 | self.tset.element_n(3) 81 | 82 | def test_elements(self): 83 | assert self.tset.elements() == [ 84 | "A", 85 | "BB", 86 | "ccc", 87 | ] 88 | 89 | def test_hash(self): 90 | assert hash(self.tset) == 3145376687 91 | 92 | 93 | class TestTextSetSetFunctions(TestTextSet): 94 | string = "A" 95 | other = TextSet("{a, BB, ccc}") 96 | 97 | @pytest.mark.parametrize( 98 | "other, expected", 99 | [(string, TextSet("{A}")), (other, TextSet("{BB, ccc}"))], 100 | ids=["string", "TextSet"], 101 | ) 102 | def test_intersection(self, other, expected): 103 | assert self.tset.intersection(other) == expected 104 | assert self.tset * other == expected 105 | 106 | @pytest.mark.parametrize( 107 | "other, expected", 108 | [(string, TextSet("{A, BB, ccc}")), (other, TextSet("{A, a, BB, ccc}"))], 109 | ids=["string", "TextSet"], 110 | ) 111 | def test_union(self, other, expected): 112 | assert self.tset.union(other) == expected 113 | assert self.tset + other == expected 114 | 115 | @pytest.mark.parametrize( 116 | "other, expected", 117 | [(string, TextSet("{BB, ccc}")), (other, TextSet("{A}"))], 118 | ids=["string", "TextSet"], 119 | ) 120 | def test_minus(self, other, expected): 121 | assert self.tset.minus(other) == expected 122 | assert self.tset - other == expected 123 | 124 | 125 | class TestTextSetComparisons(TestTextSet): 126 | other = TextSet("{2020-01-02 00:00:00+0, 2020-03-31 00:00:00+0}") 127 | 128 | def test_eq(self): 129 | _ = self.tset == self.other 130 | 131 | def test_ne(self): 132 | _ = self.tset != self.other 133 | 134 | def test_lt(self): 135 | _ = self.tset < self.other 136 | 137 | def test_le(self): 138 | _ = self.tset <= self.other 139 | 140 | def test_gt(self): 141 | _ = self.tset > self.other 142 | 143 | def test_ge(self): 144 | _ = self.tset >= self.other 145 | 146 | 147 | class TestTextSetMiscFunctions(TestTextSet): 148 | def test_hash(self): 149 | hash(self.tset) 150 | -------------------------------------------------------------------------------- /pymeos/temporal/tsequenceset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from typing import Optional, List, Union, Any, TypeVar, Type, TYPE_CHECKING 5 | 6 | from pymeos_cffi import * 7 | 8 | from .temporal import Temporal, import_pandas 9 | 10 | if TYPE_CHECKING: 11 | import pandas as pd 12 | 13 | 14 | TBase = TypeVar("TBase") 15 | TG = TypeVar("TG", bound="Temporal[Any]") 16 | TI = TypeVar("TI", bound="TInstant[Any]") 17 | TS = TypeVar("TS", bound="TSequence[Any]") 18 | TSS = TypeVar("TSS", bound="TSequenceSet[Any]") 19 | Self = TypeVar("Self", bound="TSequenceSet[Any]") 20 | 21 | 22 | class TSequenceSet(Temporal[TBase, TG, TI, TS, TSS], ABC): 23 | """ 24 | Base class for temporal sequence set types, i.e. temporal values that are 25 | defined by a set of temporal sequences. 26 | """ 27 | 28 | # ------------------------- Constructors ---------------------------------- 29 | def __init__( 30 | self, 31 | string: Optional[str] = None, 32 | *, 33 | sequence_list: Optional[List[Union[str, Any]]] = None, 34 | normalize: bool = True, 35 | _inner=None, 36 | ): 37 | assert (_inner is not None) or ( 38 | (string is not None) != (sequence_list is not None) 39 | ), "Either string must be not None or sequence_list must be not" 40 | if _inner is not None: 41 | self._inner = as_tsequenceset(_inner) 42 | elif string is not None: 43 | self._inner = as_tsequenceset(self.__class__._parse_function(string)) 44 | else: 45 | sequences = [ 46 | ( 47 | x._inner 48 | if isinstance(x, self.ComponentClass) 49 | else self.__class__._parse_function(x) 50 | ) 51 | for x in sequence_list 52 | ] 53 | count = len(sequences) 54 | self._inner = tsequenceset_make(sequences, count, normalize) 55 | 56 | @classmethod 57 | def from_sequences( 58 | cls: Type[Self], 59 | sequence_list: Optional[List[Union[str, Any]]] = None, 60 | normalize: bool = True, 61 | ) -> Self: 62 | """ 63 | Create a temporal sequence set from a list of sequences. 64 | 65 | Args: 66 | sequence_list: List of sequences. 67 | normalize: Whether to normalize the temporal sequence set. 68 | 69 | Returns: 70 | A temporal sequence set. 71 | """ 72 | return cls(sequence_list=sequence_list, normalize=normalize) 73 | 74 | # ------------------------- Accessors ------------------------------------- 75 | def num_sequences(self) -> int: 76 | """ 77 | Returns the number of sequences in ``self``. 78 | """ 79 | return temporal_num_sequences(self._inner) 80 | 81 | def start_sequence(self) -> TS: 82 | """ 83 | Returns the first sequence in ``self``. 84 | """ 85 | return self.ComponentClass(_inner=temporal_start_sequence(self._inner)) 86 | 87 | def end_sequence(self) -> TS: 88 | """ 89 | Returns the last sequence in ``self``. 90 | """ 91 | return self.ComponentClass(_inner=temporal_end_sequence(self._inner)) 92 | 93 | def sequence_n(self, n) -> TS: 94 | """ 95 | Returns the ``n``-th sequence in ``self``. 96 | """ 97 | return self.ComponentClass(_inner=temporal_sequence_n(self._inner, n + 1)) 98 | 99 | def sequences(self) -> List[TS]: 100 | """ 101 | Returns the list of sequences in ``self``. 102 | """ 103 | ss, count = temporal_sequences(self._inner) 104 | return [self.ComponentClass(_inner=ss[i]) for i in range(count)] 105 | 106 | # ------------------------- Transformations ------------------------------- 107 | def to_dataframe(self) -> pd.DataFrame: 108 | """ 109 | Returns a pandas DataFrame representation of ``self``. 110 | """ 111 | pd = import_pandas() 112 | sequences = self.sequences() 113 | data = { 114 | "sequence": [ 115 | i for i, seq in enumerate(sequences) for _ in range(seq.num_instants()) 116 | ], 117 | "time": [t for seq in sequences for t in seq.timestamps()], 118 | "value": [v for seq in sequences for v in seq.values()], 119 | } 120 | return pd.DataFrame(data).set_index(keys=["sequence", "time"]) 121 | 122 | # ------------------------- Plot Operations ------------------------------- 123 | def plot(self, *args, **kwargs): 124 | """ 125 | Plot the temporal sequence set. 126 | 127 | See Also: 128 | :meth:`pymeos.plotters.TemporalSequenceSetPlotter.plot` 129 | """ 130 | from ..plotters import TemporalSequenceSetPlotter 131 | 132 | return TemporalSequenceSetPlotter.plot(self, *args, **kwargs) 133 | -------------------------------------------------------------------------------- /pymeos/plotters/box_plotter.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot as plt 2 | 3 | from .range_plotter import SpanPlotter 4 | from .time_plotter import TimePlotter 5 | from ..boxes import TBox, STBox 6 | 7 | 8 | class BoxPlotter: 9 | """ """ 10 | 11 | @staticmethod 12 | def plot_tbox(tbox: TBox, *args, axes=None, **kwargs): 13 | """ 14 | Plot a TBox on the given axes. If the TBox has only a temporal or spatial dimension, this is equivalent 15 | to plotting the corresponding TsTzSpan or Span respectively. 16 | 17 | Params: 18 | tbox: The :class:`TBox` to plot. 19 | axes: The axes to plot on. If None, the current axes are used. 20 | *args: Additional arguments to pass to the plot function. 21 | **kwargs: Additional keyword arguments to pass to the plot function. 22 | 23 | Returns: 24 | List with the plotted elements. 25 | 26 | See Also: 27 | :func:`~pymeos.plotters.range_plotter.RangePlotter.plot_range`, 28 | :func:`~pymeos.plotters.time_plotter.TimePlotter.plot_tstzspan` 29 | """ 30 | if not tbox.has_t: 31 | return SpanPlotter.plot_span( 32 | tbox.to_floatspan(), *args, axes=axes, **kwargs 33 | ) 34 | if not tbox.has_x: 35 | return TimePlotter.plot_tstzspan( 36 | tbox.to_tstzspan(), *args, axes=axes, **kwargs 37 | ) 38 | return BoxPlotter._plot_box( 39 | tbox.tmin(), 40 | tbox.tmax(), 41 | tbox.xmin(), 42 | tbox.xmax(), 43 | *args, 44 | axes=axes, 45 | **kwargs, 46 | ) 47 | 48 | @staticmethod 49 | def plot_stbox_xy(stbox: STBox, *args, axes=None, **kwargs): 50 | """ 51 | Plot an STBox on the given axes. Plots the x and y dimensions. 52 | 53 | Params: 54 | stbox: The :class:`STBox` to plot. 55 | axes: The axes to plot on. If None, the current axes are used. 56 | *args: Additional arguments to pass to the plot function. 57 | **kwargs: Additional keyword arguments to pass to the plot function. 58 | 59 | Returns: 60 | List with the plotted elements. 61 | """ 62 | return BoxPlotter._plot_box( 63 | stbox.xmin(), 64 | stbox.xmax(), 65 | stbox.ymin(), 66 | stbox.ymax(), 67 | *args, 68 | axes=axes, 69 | **kwargs, 70 | ) 71 | 72 | @staticmethod 73 | def plot_stbox_xt(stbox: STBox, *args, axes=None, **kwargs): 74 | """ 75 | Plot an STBox on the given axes. Plots the x and t dimensions. 76 | 77 | Params: 78 | stbox: The :class:`STBox` to plot. 79 | axes: The axes to plot on. If None, the current axes are used. 80 | *args: Additional arguments to pass to the plot function. 81 | **kwargs: Additional keyword arguments to pass to the plot function. 82 | 83 | Returns: 84 | List with the plotted elements. 85 | """ 86 | return BoxPlotter._plot_box( 87 | stbox.tmin(), 88 | stbox.tmax(), 89 | stbox.xmin(), 90 | stbox.xmax(), 91 | *args, 92 | axes=axes, 93 | **kwargs, 94 | ) 95 | 96 | @staticmethod 97 | def plot_stbox_yt(stbox: STBox, *args, axes=None, **kwargs): 98 | """ 99 | Plot an STBox on the given axes. Plots the y and t dimensions. 100 | 101 | Params: 102 | stbox: The :class:`STBox` to plot. 103 | axes: The axes to plot on. If None, the current axes are used. 104 | *args: Additional arguments to pass to the plot function. 105 | **kwargs: Additional keyword arguments to pass to the plot function. 106 | 107 | Returns: 108 | List with the plotted elements. 109 | """ 110 | return BoxPlotter._plot_box( 111 | stbox.tmin(), 112 | stbox.tmax(), 113 | stbox.ymin(), 114 | stbox.ymax(), 115 | *args, 116 | axes=axes, 117 | **kwargs, 118 | ) 119 | 120 | @staticmethod 121 | def _plot_box( 122 | xmin, 123 | xmax, 124 | ymin, 125 | ymax, 126 | *args, 127 | axes=None, 128 | rotate_xticks=True, 129 | draw_filling=True, 130 | **kwargs, 131 | ): 132 | """ 133 | Plot a box on the given axes. 134 | 135 | Params: 136 | xmin: The minimum x value. 137 | xmax: The maximum x value. 138 | ymin: The minimum y value. 139 | ymax: The maximum y value. 140 | axes: The axes to plot on. If None, the current axes are used. 141 | rotate_xticks: Whether to rotate the xticks by 45 degrees. 142 | draw_filling: Whether to draw a filling. 143 | *args: Additional arguments to pass to the plot function. 144 | **kwargs: Additional keyword arguments to pass to the plot function. 145 | 146 | Returns: 147 | List with the plotted elements. 148 | """ 149 | base = axes or plt.gca() 150 | plot = base.plot( 151 | [xmin, xmax, xmax, xmin, xmin], 152 | [ymin, ymin, ymax, ymax, ymin], 153 | *args, 154 | **kwargs, 155 | ) 156 | 157 | if "color" not in kwargs: 158 | kwargs["color"] = plot[0].get_color() 159 | kwargs.pop("label", None) 160 | 161 | return_array = [plot] 162 | 163 | if draw_filling: 164 | f = base.fill_between( 165 | [xmin, xmax], [ymax, ymax], [ymin, ymin], *args, alpha=0.3, **kwargs 166 | ) 167 | return_array.append(f) 168 | 169 | if rotate_xticks: 170 | base.tick_params(axis="x", rotation=45) 171 | 172 | return return_array 173 | -------------------------------------------------------------------------------- /tests/collections/number/intset_test.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from typing import List 3 | 4 | import pytest 5 | 6 | from pymeos import IntSet, IntSpan, IntSpanSet 7 | from tests.conftest import TestPyMEOS 8 | 9 | 10 | class TestIntSet(TestPyMEOS): 11 | intset = IntSet("{1, 2, 3}") 12 | 13 | @staticmethod 14 | def assert_intset_equality(intset: IntSet, values: List[int]): 15 | assert intset.num_elements() == len(values) 16 | assert intset.elements() == values 17 | 18 | 19 | class TestIntSetConstructors(TestIntSet): 20 | def test_string_constructor(self): 21 | self.assert_intset_equality(self.intset, [1, 2, 3]) 22 | 23 | def test_list_constructor(self): 24 | intset = IntSet(elements=[1, 2, 3]) 25 | self.assert_intset_equality(intset, [1, 2, 3]) 26 | 27 | def test_hexwkb_constructor(self): 28 | intset = IntSet.from_hexwkb(IntSet(elements=[1, 2, 3]).as_hexwkb()) 29 | self.assert_intset_equality(intset, [1, 2, 3]) 30 | 31 | def test_from_as_constructor(self): 32 | assert self.intset == IntSet(str(self.intset)) 33 | assert self.intset == IntSet.from_wkb(self.intset.as_wkb()) 34 | assert self.intset == IntSet.from_hexwkb(self.intset.as_hexwkb()) 35 | 36 | def test_copy_constructor(self): 37 | intset_copy = copy(self.intset) 38 | assert self.intset == intset_copy 39 | assert self.intset is not intset_copy 40 | 41 | 42 | class TestIntSetOutputs(TestIntSet): 43 | def test_str(self): 44 | assert str(self.intset) == "{1, 2, 3}" 45 | 46 | def test_repr(self): 47 | assert repr(self.intset) == "IntSet({1, 2, 3})" 48 | 49 | def test_as_hexwkb(self): 50 | assert self.intset == IntSet.from_hexwkb(self.intset.as_hexwkb()) 51 | 52 | 53 | # class TestIntConversions(TestIntSet): 54 | 55 | # def test_to_spanset(self): 56 | # assert self.intset.to_spanset() == IntSpanSet( 57 | # '{[1, 1], [2, 2], [3, 3]}') 58 | 59 | 60 | class TestIntSetAccessors(TestIntSet): 61 | def test_to_span(self): 62 | assert self.intset.to_span() == IntSpan("[1, 3]") 63 | 64 | def test_num_elements(self): 65 | assert self.intset.num_elements() == 3 66 | 67 | def test_start_element(self): 68 | assert self.intset.start_element() == 1 69 | 70 | def test_end_element(self): 71 | assert self.intset.end_element() == 3 72 | 73 | def test_element_n(self): 74 | assert self.intset.element_n(1) == 2 75 | 76 | def test_element_n_out_of_range(self): 77 | with pytest.raises(IndexError): 78 | self.intset.element_n(3) 79 | 80 | def test_elements(self): 81 | assert self.intset.elements() == [1, 2, 3] 82 | 83 | def test_hash(self): 84 | assert hash(self.intset) == 3969573766 85 | 86 | 87 | class TestIntSetTopologicalFunctions(TestIntSet): 88 | value = 5 89 | other = IntSet("{5, 10}") 90 | 91 | @pytest.mark.parametrize( 92 | "arg, result", 93 | [ 94 | (other, False), 95 | ], 96 | ids=["other"], 97 | ) 98 | def test_is_contained_in(self, arg, result): 99 | assert self.intset.is_contained_in(arg) == result 100 | 101 | @pytest.mark.parametrize( 102 | "arg, result", 103 | [ 104 | (value, False), 105 | (other, False), 106 | ], 107 | ids=["value", "other"], 108 | ) 109 | def test_contains(self, arg, result): 110 | assert self.intset.contains(arg) == result 111 | 112 | @pytest.mark.parametrize( 113 | "arg, result", 114 | [ 115 | (other, False), 116 | ], 117 | ids=["other"], 118 | ) 119 | def test_overlaps(self, arg, result): 120 | assert self.intset.overlaps(arg) == result 121 | 122 | 123 | class TestIntSetPositionFunctions(TestIntSet): 124 | value = 5 125 | other = IntSet("{5, 10}") 126 | 127 | @pytest.mark.parametrize( 128 | "arg, result", 129 | [ 130 | (value, True), 131 | (other, True), 132 | ], 133 | ids=["value", "other"], 134 | ) 135 | def test_is_left(self, arg, result): 136 | assert self.intset.is_left(arg) == result 137 | 138 | @pytest.mark.parametrize( 139 | "arg, result", 140 | [ 141 | (value, True), 142 | (other, True), 143 | ], 144 | ids=["value", "other"], 145 | ) 146 | def test_is_over_or_left(self, arg, result): 147 | assert self.intset.is_over_or_left(arg) == result 148 | 149 | @pytest.mark.parametrize( 150 | "arg, result", 151 | [ 152 | (value, False), 153 | (other, False), 154 | ], 155 | ids=["value", "other"], 156 | ) 157 | def test_is_right(self, arg, result): 158 | assert self.intset.is_right(arg) == result 159 | 160 | @pytest.mark.parametrize( 161 | "arg, result", 162 | [ 163 | (value, False), 164 | (other, False), 165 | ], 166 | ids=["value", "other"], 167 | ) 168 | def test_is_over_or_right(self, arg, result): 169 | assert self.intset.is_over_or_right(arg) == result 170 | 171 | @pytest.mark.parametrize( 172 | "arg, result", 173 | [ 174 | (value, 2), 175 | (other, 2), 176 | ], 177 | ids=["value", "other"], 178 | ) 179 | def test_distance(self, arg, result): 180 | assert self.intset.distance(arg) == result 181 | 182 | 183 | class TestIntSetSetFunctions(TestIntSet): 184 | value = 1 185 | intset = IntSet("{1, 10}") 186 | 187 | @pytest.mark.parametrize("other", [value, intset], ids=["value", "intset"]) 188 | def test_intersection(self, other): 189 | self.intset.intersection(other) 190 | self.intset * other 191 | 192 | @pytest.mark.parametrize("other", [value, intset], ids=["value", "intset"]) 193 | def test_union(self, other): 194 | self.intset.union(other) 195 | self.intset + other 196 | 197 | @pytest.mark.parametrize("other", [value, intset], ids=["value", "intset"]) 198 | def test_minus(self, other): 199 | self.intset.minus(other) 200 | self.intset - other 201 | 202 | 203 | class TestIntSetComparisons(TestIntSet): 204 | intset = IntSet("{1, 10}") 205 | other = IntSet("{2, 10}") 206 | 207 | def test_eq(self): 208 | _ = self.intset == self.other 209 | 210 | def test_ne(self): 211 | _ = self.intset != self.other 212 | 213 | def test_lt(self): 214 | _ = self.intset < self.other 215 | 216 | def test_le(self): 217 | _ = self.intset <= self.other 218 | 219 | def test_gt(self): 220 | _ = self.intset > self.other 221 | 222 | def test_ge(self): 223 | _ = self.intset >= self.other 224 | 225 | 226 | # class TestIntSetTransformationFunctions(TestIntSet): 227 | 228 | # @pytest.mark.parametrize( 229 | # 'delta,result', 230 | # [(4, [5, 6, 8]), 231 | # (-4, [-3, -1, 0]), 232 | # ], 233 | # ids=['positive delta', 'negative delta'] 234 | # ) 235 | # def test_shift(self, delta, result): 236 | # shifted = self.intset.shift(delta) 237 | # self.assert_intset_equality(shifted, result) 238 | 239 | # @pytest.mark.parametrize( 240 | # 'delta,result', 241 | # [(6, [1, 4, 7])], 242 | # ids=['positive'] 243 | # ) 244 | # def test_scale(self, delta, result): 245 | # scaled = self.intset.scale(delta) 246 | # self.assert_intset_equality(scaled, result) 247 | 248 | # def test_shift_scale(self): 249 | # shifted_scaled = self.intset.shift_scale(4, 4) 250 | # self.assert_intset_equality(shifted_scaled, [5, 7, 9]) 251 | -------------------------------------------------------------------------------- /tests/collections/number/floatset_test.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from typing import List 3 | 4 | import pytest 5 | 6 | from pymeos import FloatSet, FloatSpan, FloatSpanSet 7 | from tests.conftest import TestPyMEOS 8 | 9 | 10 | class TestFloatSet(TestPyMEOS): 11 | floatset = FloatSet("{1, 2, 3}") 12 | 13 | @staticmethod 14 | def assert_intset_equality(floatset: FloatSet, values: List[int]): 15 | assert floatset.num_elements() == len(values) 16 | assert floatset.elements() == values 17 | 18 | 19 | class TestFloatSetConstructors(TestFloatSet): 20 | def test_string_constructor(self): 21 | self.assert_intset_equality(self.floatset, [1, 2, 3]) 22 | 23 | def test_list_constructor(self): 24 | floatset = FloatSet(elements=[1, 2, 3]) 25 | self.assert_intset_equality(floatset, [1, 2, 3]) 26 | 27 | def test_hexwkb_constructor(self): 28 | floatset = FloatSet.from_hexwkb(FloatSet(elements=[1, 2, 3]).as_hexwkb()) 29 | self.assert_intset_equality(floatset, [1, 2, 3]) 30 | 31 | def test_from_as_constructor(self): 32 | assert self.floatset == FloatSet(str(self.floatset)) 33 | assert self.floatset == FloatSet.from_wkb(self.floatset.as_wkb()) 34 | assert self.floatset == FloatSet.from_hexwkb(self.floatset.as_hexwkb()) 35 | 36 | def test_copy_constructor(self): 37 | intset_copy = copy(self.floatset) 38 | assert self.floatset == intset_copy 39 | assert self.floatset is not intset_copy 40 | 41 | 42 | class TestFloatSetOutputs(TestFloatSet): 43 | def test_str(self): 44 | assert str(self.floatset) == "{1, 2, 3}" 45 | 46 | def test_repr(self): 47 | assert repr(self.floatset) == "FloatSet({1, 2, 3})" 48 | 49 | def test_as_hexwkb(self): 50 | assert self.floatset == FloatSet.from_hexwkb(self.floatset.as_hexwkb()) 51 | 52 | 53 | # class TestIntConversions(TestFloatSet): 54 | 55 | # def test_to_spanset(self): 56 | # assert self.floatset.to_spanset() == FloatSpanSet( 57 | # '{[1, 1], [2, 2], [3, 3]}') 58 | 59 | 60 | class TestFloatSetAccessors(TestFloatSet): 61 | def test_to_span(self): 62 | assert self.floatset.to_span() == FloatSpan("[1, 3]") 63 | 64 | def test_num_elements(self): 65 | assert self.floatset.num_elements() == 3 66 | 67 | def test_start_element(self): 68 | assert self.floatset.start_element() == 1 69 | 70 | def test_end_element(self): 71 | assert self.floatset.end_element() == 3 72 | 73 | def test_element_n(self): 74 | assert self.floatset.element_n(1) == 2 75 | 76 | def test_element_n_out_of_range(self): 77 | with pytest.raises(IndexError): 78 | self.floatset.element_n(3) 79 | 80 | def test_elements(self): 81 | assert self.floatset.elements() == [1, 2, 3] 82 | 83 | def test_hash(self): 84 | assert hash(self.floatset) == 2419122126 85 | 86 | 87 | class TestFloatSetTopologicalFunctions(TestFloatSet): 88 | value = 5.0 89 | other = FloatSet("{5, 10}") 90 | 91 | @pytest.mark.parametrize( 92 | "arg, result", 93 | [ 94 | (other, False), 95 | ], 96 | ids=["other"], 97 | ) 98 | def test_is_contained_in(self, arg, result): 99 | assert self.floatset.is_contained_in(arg) == result 100 | 101 | @pytest.mark.parametrize( 102 | "arg, result", 103 | [ 104 | (value, False), 105 | (other, False), 106 | ], 107 | ids=["value", "other"], 108 | ) 109 | def test_contains(self, arg, result): 110 | assert self.floatset.contains(arg) == result 111 | 112 | @pytest.mark.parametrize( 113 | "arg, result", 114 | [ 115 | (other, False), 116 | ], 117 | ids=["other"], 118 | ) 119 | def test_overlaps(self, arg, result): 120 | assert self.floatset.overlaps(arg) == result 121 | 122 | 123 | class TestFloatSetPositionFunctions(TestFloatSet): 124 | value = 5.0 125 | other = FloatSet("{5, 10}") 126 | 127 | @pytest.mark.parametrize( 128 | "arg, result", 129 | [ 130 | (value, True), 131 | (other, True), 132 | ], 133 | ids=["value", "other"], 134 | ) 135 | def test_is_left(self, arg, result): 136 | assert self.floatset.is_left(arg) == result 137 | 138 | @pytest.mark.parametrize( 139 | "arg, result", 140 | [ 141 | (value, True), 142 | (other, True), 143 | ], 144 | ids=["value", "other"], 145 | ) 146 | def test_is_over_or_left(self, arg, result): 147 | assert self.floatset.is_over_or_left(arg) == result 148 | 149 | @pytest.mark.parametrize( 150 | "arg, result", 151 | [ 152 | (value, False), 153 | (other, False), 154 | ], 155 | ids=["value", "other"], 156 | ) 157 | def test_is_right(self, arg, result): 158 | assert self.floatset.is_right(arg) == result 159 | 160 | @pytest.mark.parametrize( 161 | "arg, result", 162 | [ 163 | (value, False), 164 | (other, False), 165 | ], 166 | ids=["value", "other"], 167 | ) 168 | def test_is_over_or_right(self, arg, result): 169 | assert self.floatset.is_over_or_right(arg) == result 170 | 171 | @pytest.mark.parametrize( 172 | "arg, result", 173 | [ 174 | (value, 2.0), 175 | (other, 2.0), 176 | ], 177 | ids=["value", "other"], 178 | ) 179 | def test_distance(self, arg, result): 180 | assert self.floatset.distance(arg) == result 181 | 182 | 183 | class TestFloatSetSetFunctions(TestFloatSet): 184 | value = 5.0 185 | floatset = FloatSet("{1, 10}") 186 | 187 | @pytest.mark.parametrize("other", [value, floatset], ids=["value", "floatset"]) 188 | def test_intersection(self, other): 189 | self.floatset.intersection(other) 190 | self.floatset * other 191 | 192 | @pytest.mark.parametrize("other", [value, floatset], ids=["value", "floatset"]) 193 | def test_union(self, other): 194 | self.floatset.union(other) 195 | self.floatset + other 196 | 197 | @pytest.mark.parametrize("other", [value, floatset], ids=["value", "floatset"]) 198 | def test_minus(self, other): 199 | self.floatset.minus(other) 200 | self.floatset - other 201 | 202 | 203 | class TestFloatSetComparisons(TestFloatSet): 204 | floatset = FloatSet("{1, 10}") 205 | other = FloatSet("{2, 10}") 206 | 207 | def test_eq(self): 208 | _ = self.floatset == self.other 209 | 210 | def test_ne(self): 211 | _ = self.floatset != self.other 212 | 213 | def test_lt(self): 214 | _ = self.floatset < self.other 215 | 216 | def test_le(self): 217 | _ = self.floatset <= self.other 218 | 219 | def test_gt(self): 220 | _ = self.floatset > self.other 221 | 222 | def test_ge(self): 223 | _ = self.floatset >= self.other 224 | 225 | 226 | # class TestFloatSetTransformationFunctions(TestFloatSet): 227 | 228 | # @pytest.mark.parametrize( 229 | # 'delta,result', 230 | # [(4, [5, 6, 8]), 231 | # (-4, [-3, -1, 0]), 232 | # ], 233 | # ids=['positive delta', 'negative delta'] 234 | # ) 235 | # def test_shift(self, delta, result): 236 | # shifted = self.floatset.shift(delta) 237 | # self.assert_intset_equality(shifted, result) 238 | 239 | # @pytest.mark.parametrize( 240 | # 'delta,result', 241 | # [(6, [1, 4, 7])], 242 | # ids=['positive'] 243 | # ) 244 | # def test_scale(self, delta, result): 245 | # scaled = self.floatset.scale(delta) 246 | # self.assert_intset_equality(scaled, result) 247 | 248 | # def test_shift_scale(self): 249 | # shifted_scaled = self.floatset.shift_scale(4, 4) 250 | # self.assert_intset_equality(shifted_scaled, [5, 7, 9]) 251 | -------------------------------------------------------------------------------- /pymeos/collections/text/textset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional, overload, Union 4 | 5 | from pymeos_cffi import * 6 | 7 | from ..base import Set 8 | 9 | 10 | class TextSet(Set[str]): 11 | """ 12 | Class for representing a set of text values. 13 | 14 | ``TextSet`` objects can be created with a single argument of type string as 15 | in MobilityDB. 16 | 17 | >>> TextSet(string='{a, b, c, def}') 18 | 19 | Another possibility is to create a ``TextSet`` object from a list of strings. 20 | 21 | >>> TextSet(elements=['a', 'b', 'c', 'def']) 22 | 23 | 24 | """ 25 | 26 | __slots__ = ["_inner"] 27 | 28 | _mobilitydb_name = "textset" 29 | 30 | _parse_function = textset_in 31 | _parse_value_function = lambda x: x 32 | _make_function = textset_make 33 | 34 | # ------------------------- Constructors ---------------------------------- 35 | 36 | # ------------------------- Output ---------------------------------------- 37 | 38 | def __str__(self): 39 | """ 40 | Return the string representation of the content of ``self``. 41 | 42 | Returns: 43 | A new :class:`str` instance 44 | 45 | MEOS Functions: 46 | textset_out 47 | """ 48 | return textset_out(self._inner) 49 | 50 | # ------------------------- Conversions ----------------------------------- 51 | 52 | def to_spanset(self): 53 | raise NotImplementedError() 54 | 55 | def to_span(self): 56 | raise NotImplementedError() 57 | 58 | # ------------------------- Accessors ------------------------------------- 59 | 60 | def start_element(self): 61 | """ 62 | Returns the first element in ``self``. 63 | 64 | Returns: 65 | A :class:`str` instance 66 | 67 | MEOS Functions: 68 | textset_start_value 69 | """ 70 | return textset_start_value(self._inner) 71 | 72 | def end_element(self): 73 | """ 74 | Returns the last element in ``self``. 75 | 76 | Returns: 77 | A :class:`str` instance 78 | 79 | MEOS Functions: 80 | textset_end_value 81 | """ 82 | return textset_end_value(self._inner) 83 | 84 | def element_n(self, n: int): 85 | """ 86 | Returns the ``n``-th element in ``self``. 87 | 88 | Args: 89 | n: The 0-based index of the element to return. 90 | 91 | Returns: 92 | A :class:`str` instance 93 | 94 | MEOS Functions: 95 | textset_value_n 96 | """ 97 | super().element_n(n) 98 | return text2cstring(textset_value_n(self._inner, n + 1)[0]) 99 | 100 | def elements(self): 101 | """ 102 | Returns the elements in ``self``. 103 | 104 | Returns: 105 | A list of :class:`str` instances 106 | 107 | MEOS Functions: 108 | textset_values 109 | """ 110 | elems = textset_values(self._inner) 111 | return [text2cstring(elems[i]) for i in range(self.num_elements())] 112 | 113 | # ------------------------- Topological Operations -------------------------------- 114 | 115 | def contains(self, content: Union[TextSet, str]) -> bool: 116 | """ 117 | Returns whether ``self`` contains ``content``. 118 | 119 | Args: 120 | content: object to compare with 121 | 122 | Returns: 123 | True if contains, False otherwise 124 | 125 | MEOS Functions: 126 | contains_set_set, contains_set_text 127 | """ 128 | if isinstance(content, str): 129 | return contains_set_text(self._inner, content) 130 | else: 131 | return super().contains(content) 132 | 133 | # ------------------------- Transformations -------------------------------- 134 | def lowercase(self): 135 | """ 136 | Returns a new textset that is the result of appling uppercase to ``self`` 137 | 138 | Returns: 139 | A :class:`str` instance 140 | 141 | MEOS Functions: 142 | textset_lower 143 | """ 144 | return self.__class__(_inner=textset_lower(self._inner)) 145 | 146 | def uppercase(self): 147 | """ 148 | Returns a new textset that is the result of appling uppercase to ``self`` 149 | 150 | Returns: 151 | A :class:`str` instance 152 | 153 | MEOS Functions: 154 | textset_upper 155 | """ 156 | return self.__class__(_inner=textset_upper(self._inner)) 157 | 158 | # ------------------------- Set Operations -------------------------------- 159 | 160 | @overload 161 | def intersection(self, other: str) -> Optional[str]: ... 162 | 163 | @overload 164 | def intersection(self, other: TextSet) -> Optional[TextSet]: ... 165 | 166 | def intersection(self, other): 167 | """ 168 | Returns the intersection of ``self`` and ``other``. 169 | 170 | Args: 171 | other: A :class:`TextSet` or :class:`str` instance 172 | 173 | Returns: 174 | An object of the same type as ``other`` or ``None`` if the intersection is empty. 175 | 176 | MEOS Functions: 177 | intersection_set_text, intersection_set_set 178 | """ 179 | if isinstance(other, str): 180 | result = intersection_set_text(self._inner, other) 181 | return TextSet(_inner=result) if result is not None else None 182 | elif isinstance(other, TextSet): 183 | result = intersection_set_set(self._inner, other._inner) 184 | return TextSet(_inner=result) if result is not None else None 185 | else: 186 | return super().intersection(other) 187 | 188 | def minus(self, other: Union[TextSet, str]) -> Optional[TextSet]: 189 | """ 190 | Returns the difference of ``self`` and ``other``. 191 | 192 | Args: 193 | other: A :class:`TextSet` or :class:`str` instance 194 | 195 | Returns: 196 | A :class:`TextSet` instance or ``None`` if the difference is empty. 197 | 198 | MEOS Functions: 199 | minus_set_text, minus_set_set 200 | """ 201 | if isinstance(other, str): 202 | result = minus_set_text(self._inner, other) 203 | return TextSet(_inner=result) if result is not None else None 204 | elif isinstance(other, TextSet): 205 | result = minus_set_set(self._inner, other._inner) 206 | return TextSet(_inner=result) if result is not None else None 207 | else: 208 | return super().minus(other) 209 | 210 | def subtract_from(self, other: str) -> Optional[str]: 211 | """ 212 | Returns the difference of ``other`` and ``self``. 213 | 214 | Args: 215 | other: A :class:`str` instance 216 | 217 | Returns: 218 | A :class:`str` instance. 219 | 220 | MEOS Functions: 221 | minus_geo_set 222 | 223 | See Also: 224 | :meth:`minus` 225 | """ 226 | result = minus_text_set(other, self._inner) 227 | return text2cstring(result[0]) if result is not None else None 228 | 229 | def union(self, other: Union[TextSet, str]) -> TextSet: 230 | """ 231 | Returns the union of ``self`` and ``other``. 232 | 233 | Args: 234 | other: A :class:`TextSet` or :class:`str` instance 235 | 236 | Returns: 237 | A :class:`TextSet` instance. 238 | 239 | MEOS Functions: 240 | union_set_text, union_set_set 241 | """ 242 | if isinstance(other, str): 243 | result = union_set_text(self._inner, other) 244 | return TextSet(_inner=result) if result is not None else None 245 | elif isinstance(other, TextSet): 246 | result = union_set_set(self._inner, other._inner) 247 | return TextSet(_inner=result) if result is not None else None 248 | else: 249 | return super().union(other) 250 | -------------------------------------------------------------------------------- /tests/collections/number/intspan_test.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | import pytest 4 | 5 | from pymeos import IntSpan, IntSpanSet 6 | 7 | from tests.conftest import TestPyMEOS 8 | 9 | 10 | class TestIntSpan(TestPyMEOS): 11 | intspan = IntSpan("[7, 10)") 12 | 13 | @staticmethod 14 | def assert_intspan_equality( 15 | intspan: IntSpan, 16 | lower: int = None, 17 | upper: int = None, 18 | lower_inc: bool = None, 19 | upper_inc: bool = None, 20 | ): 21 | if lower is not None: 22 | assert intspan.lower() == lower 23 | if upper is not None: 24 | assert intspan.upper() == upper 25 | if lower_inc is not None: 26 | assert intspan.lower_inc() == lower_inc 27 | if upper_inc is not None: 28 | assert intspan.upper_inc() == upper_inc 29 | 30 | 31 | class TestIntSpanConstructors(TestIntSpan): 32 | @pytest.mark.parametrize( 33 | "source, params", 34 | [ 35 | ("(7, 10)", (8, 10, True, False)), 36 | ("[7, 10]", (7, 11, True, False)), 37 | ], 38 | ) 39 | def test_string_constructor(self, source, params): 40 | intspan = IntSpan(source) 41 | self.assert_intspan_equality(intspan, *params) 42 | 43 | @pytest.mark.parametrize( 44 | "input_lower,input_upper,lower,upper", 45 | [ 46 | ("7", "10", 7, 10), 47 | (7, 10, 7, 10), 48 | (7, "10", 7, 10), 49 | ], 50 | ids=["string", "int", "mixed"], 51 | ) 52 | def test_constructor_bounds(self, input_lower, input_upper, lower, upper): 53 | intspan = IntSpan(lower=lower, upper=upper) 54 | self.assert_intspan_equality(intspan, lower, upper) 55 | 56 | def test_constructor_bound_inclusivity_defaults(self): 57 | intspan = IntSpan(lower="7", upper="10") 58 | self.assert_intspan_equality(intspan, lower_inc=True, upper_inc=False) 59 | 60 | @pytest.mark.parametrize( 61 | "lower,upper", 62 | [ 63 | (True, True), 64 | (True, False), 65 | (False, True), 66 | (False, False), 67 | ], 68 | ) 69 | def test_constructor_bound_inclusivity(self, lower, upper): 70 | intspan = IntSpan(lower="7", upper="10", lower_inc=lower, upper_inc=upper) 71 | self.assert_intspan_equality(intspan, lower_inc=True, upper_inc=False) 72 | 73 | def test_hexwkb_constructor(self): 74 | assert self.intspan == IntSpan.from_hexwkb(self.intspan.as_hexwkb()) 75 | 76 | def test_from_as_constructor(self): 77 | assert self.intspan == IntSpan(str(self.intspan)) 78 | assert self.intspan == IntSpan.from_wkb(self.intspan.as_wkb()) 79 | assert self.intspan == IntSpan.from_hexwkb(self.intspan.as_hexwkb()) 80 | 81 | def test_copy_constructor(self): 82 | other = copy(self.intspan) 83 | assert self.intspan == other 84 | assert self.intspan is not other 85 | 86 | 87 | class TestIntSpanOutputs(TestIntSpan): 88 | def test_str(self): 89 | assert str(self.intspan) == "[7, 10)" 90 | 91 | def test_repr(self): 92 | assert repr(self.intspan) == "IntSpan([7, 10))" 93 | 94 | def test_hexwkb(self): 95 | assert self.intspan == IntSpan.from_hexwkb(self.intspan.as_hexwkb()) 96 | 97 | 98 | class TestIntSpanConversions(TestIntSpan): 99 | def test_to_intspanset(self): 100 | intspanset = self.intspan.to_spanset() 101 | assert isinstance(intspanset, IntSpanSet) 102 | assert intspanset.num_spans() == 1 103 | assert intspanset.start_span() == self.intspan 104 | 105 | 106 | class TestIntSpanAccessors(TestIntSpan): 107 | intspan2 = IntSpan("[8, 11]") 108 | 109 | def test_lower(self): 110 | assert self.intspan.lower() == 7 111 | assert self.intspan2.lower() == 8 112 | 113 | def test_upper(self): 114 | assert self.intspan.upper() == 10 115 | assert self.intspan2.upper() == 12 116 | 117 | def test_lower_inc(self): 118 | assert self.intspan.lower_inc() 119 | assert self.intspan2.lower_inc() 120 | 121 | def test_upper_inc(self): 122 | assert not self.intspan.upper_inc() 123 | assert not self.intspan2.upper_inc() 124 | 125 | def test_width(self): 126 | assert self.intspan.width() == 3 127 | assert self.intspan2.width() == 4 128 | 129 | def test_hash(self): 130 | assert hash(self.intspan) 131 | 132 | 133 | class TestIntSpanTransformations(TestIntSpan): 134 | @pytest.mark.parametrize( 135 | "delta,result", 136 | [ 137 | (4, (11, 14, True, False)), 138 | (-4, (3, 6, True, False)), 139 | ], 140 | ids=["positive delta", "negative delta"], 141 | ) 142 | def test_shift(self, delta, result): 143 | shifted = self.intspan.shift(delta) 144 | self.assert_intspan_equality(shifted, *result) 145 | 146 | @pytest.mark.parametrize( 147 | "delta,result", 148 | [ 149 | (4, (7, 12, True, False)), 150 | ], 151 | ids=["positive"], 152 | ) 153 | def test_scale(self, delta, result): 154 | scaled = self.intspan.scale(delta) 155 | self.assert_intspan_equality(scaled, *result) 156 | 157 | def test_shift_scale(self): 158 | shifted_scaled = self.intspan.shift_scale(4, 2) 159 | self.assert_intspan_equality(shifted_scaled, 11, 14, True, False) 160 | 161 | 162 | class TestIntSpanTopologicalPositionFunctions(TestIntSpan): 163 | value = 5 164 | intspan = IntSpan("(1, 20)") 165 | intspanset = IntSpanSet("{(1, 20), (31, 41)}") 166 | 167 | @pytest.mark.parametrize( 168 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 169 | ) 170 | def test_is_adjacent(self, other): 171 | self.intspan.is_adjacent(other) 172 | 173 | @pytest.mark.parametrize( 174 | "other", [intspan, intspanset], ids=["intspan", "intspanset"] 175 | ) 176 | def test_is_contained_in(self, other): 177 | self.intspan.is_contained_in(other) 178 | 179 | @pytest.mark.parametrize( 180 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 181 | ) 182 | def test_contains(self, other): 183 | self.intspan.contains(other) 184 | _ = other in self.intspan 185 | 186 | @pytest.mark.parametrize( 187 | "other", [intspan, intspanset], ids=["intspan", "intspanset"] 188 | ) 189 | def test_overlaps(self, other): 190 | self.intspan.overlaps(other) 191 | 192 | @pytest.mark.parametrize( 193 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 194 | ) 195 | def test_is_same(self, other): 196 | self.intspan.is_same(other) 197 | 198 | @pytest.mark.parametrize( 199 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 200 | ) 201 | def test_is_left(self, other): 202 | self.intspan.is_left(other) 203 | 204 | @pytest.mark.parametrize( 205 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 206 | ) 207 | def test_is_over_or_left(self, other): 208 | self.intspan.is_over_or_left(other) 209 | 210 | @pytest.mark.parametrize( 211 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 212 | ) 213 | def test_is_right(self, other): 214 | self.intspan.is_right(other) 215 | 216 | @pytest.mark.parametrize( 217 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 218 | ) 219 | def test_is_over_or_right(self, other): 220 | self.intspan.is_over_or_right(other) 221 | 222 | @pytest.mark.parametrize( 223 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 224 | ) 225 | def test_distance(self, other): 226 | self.intspan.distance(other) 227 | 228 | 229 | class TestIntSpanSetFunctions(TestIntSpan): 230 | value = 1 231 | intspan = IntSpan("(1, 20)") 232 | intspanset = IntSpanSet("{(1, 20), (31, 41)}") 233 | 234 | @pytest.mark.parametrize( 235 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 236 | ) 237 | def test_intersection(self, other): 238 | self.intspan.intersection(other) 239 | self.intspan * other 240 | 241 | @pytest.mark.parametrize( 242 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 243 | ) 244 | def test_union(self, other): 245 | self.intspan.union(other) 246 | self.intspan + other 247 | 248 | @pytest.mark.parametrize( 249 | "other", [value, intspan, intspanset], ids=["value", "intspan", "intspanset"] 250 | ) 251 | def test_minus(self, other): 252 | self.intspan.minus(other) 253 | self.intspan - other 254 | 255 | 256 | class TestIntSpanComparisons(TestIntSpan): 257 | intspan = IntSpan("(1, 20)") 258 | other = IntSpan("[5, 10)") 259 | 260 | def test_eq(self): 261 | _ = self.intspan == self.other 262 | 263 | def test_ne(self): 264 | _ = self.intspan != self.other 265 | 266 | def test_lt(self): 267 | _ = self.intspan < self.other 268 | 269 | def test_le(self): 270 | _ = self.intspan <= self.other 271 | 272 | def test_gt(self): 273 | _ = self.intspan > self.other 274 | 275 | def test_ge(self): 276 | _ = self.intspan >= self.other 277 | -------------------------------------------------------------------------------- /tests/collections/time/dateset_test.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from datetime import date, timedelta 3 | 4 | import pytest 5 | 6 | from pymeos import ( 7 | DateSpan, 8 | DateSpanSet, 9 | DateSet, 10 | ) 11 | from tests.conftest import TestPyMEOS 12 | 13 | 14 | class TestDateSet(TestPyMEOS): 15 | date_set = DateSet("{2019-09-25, 2019-09-26, 2019-09-27}") 16 | 17 | 18 | class TestDateSetConstructors(TestDateSet): 19 | def test_string_constructor(self): 20 | assert isinstance(self.date_set, DateSet) 21 | assert self.date_set.elements() == [ 22 | date(2019, 9, 25), 23 | date(2019, 9, 26), 24 | date(2019, 9, 27), 25 | ] 26 | 27 | def test_list_constructor(self): 28 | d_set = DateSet( 29 | elements=[ 30 | date(2019, 9, 25), 31 | date(2019, 9, 26), 32 | date(2019, 9, 27), 33 | ] 34 | ) 35 | assert self.date_set == d_set 36 | 37 | def test_from_as_constructor(self): 38 | assert self.date_set == DateSet(str(self.date_set)) 39 | assert self.date_set == DateSet.from_wkb(self.date_set.as_wkb()) 40 | assert self.date_set == DateSet.from_hexwkb(self.date_set.as_hexwkb()) 41 | 42 | def test_copy_constructor(self): 43 | date_set_copy = copy(self.date_set) 44 | assert self.date_set == date_set_copy 45 | assert self.date_set is not date_set_copy 46 | 47 | 48 | class TestDateSetOutputs(TestDateSet): 49 | def test_str(self): 50 | assert str(self.date_set) == "{2019-09-25, 2019-09-26, 2019-09-27}" 51 | 52 | def test_repr(self): 53 | assert repr(self.date_set) == "DateSet({2019-09-25, 2019-09-26, 2019-09-27})" 54 | 55 | def test_as_hexwkb(self): 56 | assert self.date_set == DateSet.from_hexwkb(self.date_set.as_hexwkb()) 57 | 58 | 59 | class TestCollectionConversions(TestDateSet): 60 | def test_to_span(self): 61 | assert self.date_set.to_span() == DateSpan("[2019-09-25, 2019-09-27]") 62 | 63 | def test_to_spanset(self): 64 | expected = DateSpanSet( 65 | "{[2019-09-25, 2019-09-25], " 66 | "[2019-09-26, 2019-09-26], " 67 | "[2019-09-27, 2019-09-27]}" 68 | ) 69 | 70 | spanset = self.date_set.to_spanset() 71 | 72 | assert spanset == expected 73 | 74 | 75 | class TestDateSetAccessors(TestDateSet): 76 | def test_duration(self): 77 | assert self.date_set.duration() == timedelta(days=3) 78 | 79 | def test_num_elements(self): 80 | assert self.date_set.num_elements() == 3 81 | assert len(self.date_set) == 3 82 | 83 | def test_start_element(self): 84 | assert self.date_set.start_element() == date(2019, 9, 25) 85 | 86 | def test_end_element(self): 87 | assert self.date_set.end_element() == date(2019, 9, 27) 88 | 89 | def test_element_n(self): 90 | assert self.date_set.element_n(1) == date(2019, 9, 26) 91 | 92 | def test_element_n_out_of_range(self): 93 | with pytest.raises(IndexError): 94 | self.date_set.element_n(3) 95 | 96 | def test_elements(self): 97 | assert self.date_set.elements() == [ 98 | date(2019, 9, 25), 99 | date(2019, 9, 26), 100 | date(2019, 9, 27), 101 | ] 102 | 103 | 104 | class TestDateSetPositionFunctions(TestDateSet): 105 | date_value = date(year=2019, month=9, day=25) 106 | other_date_set = DateSet("{2020-01-01, 2020-01-31}") 107 | 108 | @pytest.mark.parametrize( 109 | "other, expected", 110 | [(other_date_set, False)], 111 | ids=["dateset"], 112 | ) 113 | def test_is_contained_in(self, other, expected): 114 | assert self.date_set.is_contained_in(other) == expected 115 | 116 | @pytest.mark.parametrize( 117 | "other, expected", 118 | [(date_value, True), (other_date_set, False)], 119 | ids=["date", "dateset"], 120 | ) 121 | def test_contains(self, other, expected): 122 | assert self.date_set.contains(other) == expected 123 | assert (other in self.date_set) == expected 124 | 125 | @pytest.mark.parametrize( 126 | "other", 127 | [other_date_set], 128 | ids=["dateset"], 129 | ) 130 | def test_overlaps(self, other): 131 | self.date_set.overlaps(other) 132 | 133 | @pytest.mark.parametrize( 134 | "other", [date_value, other_date_set], ids=["date", "dateset"] 135 | ) 136 | def test_is_before(self, other): 137 | self.date_set.is_before(other) 138 | 139 | @pytest.mark.parametrize( 140 | "other", [date_value, other_date_set], ids=["date", "dateset"] 141 | ) 142 | def test_is_over_or_before(self, other): 143 | self.date_set.is_over_or_before(other) 144 | 145 | @pytest.mark.parametrize( 146 | "other", [date_value, other_date_set], ids=["date", "dateset"] 147 | ) 148 | def test_is_after(self, other): 149 | self.date_set.is_after(other) 150 | 151 | @pytest.mark.parametrize( 152 | "other", [date_value, other_date_set], ids=["date", "dateset"] 153 | ) 154 | def test_is_over_or_after(self, other): 155 | self.date_set.is_over_or_after(other) 156 | 157 | @pytest.mark.parametrize( 158 | "other", [date_value, other_date_set], ids=["date", "dateset"] 159 | ) 160 | def test_distance(self, other): 161 | self.date_set.distance(other) 162 | 163 | 164 | class TestDateSetSetFunctions(TestDateSet): 165 | date_value = date(year=2020, month=1, day=1) 166 | dateset = DateSet("{2020-01-01, 2020-01-31}") 167 | datespan = DateSpan("(2020-01-01, 2020-01-31)") 168 | datespanset = DateSpanSet("{(2020-01-01, 2020-01-31), (2021-01-01, 2021-01-31)}") 169 | 170 | @pytest.mark.parametrize( 171 | "other", 172 | [datespan, datespanset, date_value, dateset], 173 | ids=["datespan", "datespanset", "date", "dateset"], 174 | ) 175 | def test_intersection(self, other): 176 | self.dateset.intersection(other) 177 | self.dateset * other 178 | 179 | @pytest.mark.parametrize( 180 | "other", 181 | [datespan, datespanset, date_value, dateset], 182 | ids=["datespan", "datespanset", "date", "dateset"], 183 | ) 184 | def test_union(self, other): 185 | self.dateset.union(other) 186 | self.dateset + other 187 | 188 | @pytest.mark.parametrize( 189 | "other", 190 | [datespan, datespanset, date_value, dateset], 191 | ids=["datespan", "datespanset", "date", "dateset"], 192 | ) 193 | def test_minus(self, other): 194 | self.dateset.minus(other) 195 | self.dateset - other 196 | 197 | 198 | class TestDateSetComparisons(TestDateSet): 199 | dateset = DateSet("{2020-01-01, 2020-01-31}") 200 | other = DateSet("{2020-01-02, 2020-03-31}") 201 | 202 | def test_eq(self): 203 | assert not self.dateset == self.other 204 | 205 | def test_ne(self): 206 | assert self.dateset != self.other 207 | 208 | def test_lt(self): 209 | assert self.dateset < self.other 210 | 211 | def test_le(self): 212 | assert self.dateset <= self.other 213 | 214 | def test_gt(self): 215 | assert not self.dateset > self.other 216 | 217 | def test_ge(self): 218 | assert not self.dateset >= self.other 219 | 220 | 221 | class TestDateSetFunctionsFunctions(TestDateSet): 222 | dateset = DateSet("{2020-01-01, 2020-01-02, 2020-01-04}") 223 | 224 | @pytest.mark.parametrize( 225 | "delta,expected", 226 | [ 227 | ( 228 | timedelta(days=4), 229 | DateSet("{2020-1-5, 2020-1-6, 2020-1-8}"), 230 | ), 231 | ( 232 | timedelta(days=-4), 233 | DateSet("{2019-12-28,2019-12-29, 2019-12-31}"), 234 | ), 235 | ( 236 | 4, 237 | DateSet("{2020-1-5, 2020-1-6, 2020-1-8}"), 238 | ), 239 | ( 240 | -4, 241 | DateSet("{2019-12-28,2019-12-29, 2019-12-31}"), 242 | ), 243 | ], 244 | ids=[ 245 | "positive timedelta", 246 | "negative timedelta", 247 | "positive int", 248 | "negative int", 249 | ], 250 | ) 251 | def test_shift(self, delta, expected): 252 | shifted = self.dateset.shift(delta) 253 | assert shifted == expected 254 | 255 | @pytest.mark.parametrize( 256 | "delta", 257 | [timedelta(days=6), 6], 258 | ids=["timedelta", "int"], 259 | ) 260 | def test_scale(self, delta): 261 | expected = DateSet("{2020-1-1, 2020-1-3, 2020-1-8}") 262 | 263 | scaled = self.dateset.scale(delta) 264 | 265 | assert scaled == expected 266 | 267 | @pytest.mark.parametrize( 268 | "shift, scale", 269 | [ 270 | (timedelta(days=4), timedelta(days=4)), 271 | (timedelta(days=4), 4), 272 | (4, timedelta(days=4)), 273 | (4, 4), 274 | ], 275 | ids=[ 276 | "timedelta timedelta", 277 | "timedelta int", 278 | "int timedelta", 279 | "int int", 280 | ], 281 | ) 282 | def test_shift_scale(self, shift, scale): 283 | shifted_scaled = self.dateset.shift_scale(shift, scale) 284 | assert shifted_scaled == DateSet("{2020-01-05, 2020-01-06, 2020-01-10}") 285 | 286 | 287 | class TestDateSetMiscFunctions(TestDateSet): 288 | dateset = DateSet("{2020-01-01, 2020-01-02, 2020-01-04}") 289 | 290 | def test_hash(self): 291 | assert hash(self.dateset) 292 | -------------------------------------------------------------------------------- /pymeos/collections/geo/geoset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from typing import List, overload, Optional, Union, TypeVar 5 | 6 | import shapely as shp 7 | from pymeos_cffi import ( 8 | geoset_start_value, 9 | gserialized_to_shapely_geometry, 10 | geoset_end_value, 11 | geoset_value_n, 12 | geoset_values, 13 | intersection_set_geo, 14 | minus_set_geo, 15 | union_set_geo, 16 | geoset_as_ewkt, 17 | geoset_as_text, 18 | geoset_out, 19 | geoset_make, 20 | geoset_srid, 21 | geoset_round, 22 | minus_geo_set, 23 | geomset_in, 24 | geogset_in, 25 | pgis_geometry_in, 26 | geometry_to_gserialized, 27 | pgis_geography_in, 28 | geography_to_gserialized, 29 | intersection_set_set, 30 | minus_set_set, 31 | union_set_set, 32 | ) 33 | 34 | from ..base import Set 35 | 36 | Self = TypeVar("Self", bound="GeoSet") 37 | 38 | 39 | class GeoSet(Set[shp.Geometry], ABC): 40 | __slots__ = ["_inner"] 41 | 42 | _make_function = geoset_make 43 | 44 | # ------------------------- Constructors ---------------------------------- 45 | 46 | # ------------------------- Output ---------------------------------------- 47 | 48 | def __str__(self, max_decimals: int = 15): 49 | """ 50 | Return the string representation of the content of ``self``. 51 | 52 | Returns: 53 | A new :class:`str` instance 54 | 55 | MEOS Functions: 56 | geoset_out 57 | """ 58 | return geoset_out(self._inner, max_decimals) 59 | 60 | def as_ewkt(self, max_decimals: int = 15) -> str: 61 | """ 62 | Returns the EWKT representation of ``self``. 63 | 64 | Args: 65 | max_decimals: The number of decimal places to use for the coordinates. 66 | 67 | Returns: 68 | A :class:`str` instance. 69 | 70 | MEOS Functions: 71 | geoset_as_ewkt 72 | """ 73 | return geoset_as_ewkt(self._inner, max_decimals) 74 | 75 | def as_wkt(self, max_decimals: int = 15): 76 | """ 77 | Returns the WKT representation of ``self``. 78 | 79 | Args: 80 | max_decimals: The number of decimal places to use for the coordinates. 81 | 82 | Returns: 83 | A :class:`str` instance. 84 | 85 | MEOS Functions: 86 | geoset_as_text 87 | """ 88 | return geoset_as_text(self._inner, max_decimals) 89 | 90 | def as_text(self, max_decimals: int = 15): 91 | """ 92 | Returns the WKT representation of ``self``. 93 | 94 | Args: 95 | max_decimals: The number of decimal places to use for the coordinates. 96 | 97 | Returns: 98 | A :class:`str` instance. 99 | 100 | MEOS Functions: 101 | geoset_as_text 102 | """ 103 | return geoset_as_text(self._inner, max_decimals) 104 | 105 | # ------------------------- Conversions ----------------------------------- 106 | 107 | def to_spanset(self): 108 | raise NotImplementedError() 109 | 110 | def to_span(self): 111 | raise NotImplementedError() 112 | 113 | # ------------------------- Accessors ------------------------------------- 114 | 115 | def start_element(self) -> shp.Geometry: 116 | """ 117 | Returns the first element in ``self``. 118 | 119 | Returns: 120 | A :class:`Geometry` instance 121 | 122 | MEOS Functions: 123 | geoset_start_value 124 | """ 125 | return gserialized_to_shapely_geometry(geoset_start_value(self._inner)) 126 | 127 | def end_element(self) -> shp.Geometry: 128 | """ 129 | Returns the last element in ``self``. 130 | 131 | Returns: 132 | A :class:`Geometry` instance 133 | 134 | MEOS Functions: 135 | geoset_end_value 136 | """ 137 | return gserialized_to_shapely_geometry(geoset_end_value(self._inner)) 138 | 139 | def element_n(self, n: int) -> shp.Geometry: 140 | """ 141 | Returns the ``n``-th element in ``self``. 142 | 143 | Args: 144 | n: The 0-based index of the element to return. 145 | 146 | Returns: 147 | A :class:`Geometry` instance 148 | 149 | MEOS Functions: 150 | geoset_value_n 151 | """ 152 | super().element_n(n) 153 | return gserialized_to_shapely_geometry(geoset_value_n(self._inner, n + 1)[0]) 154 | 155 | def elements(self) -> List[shp.Geometry]: 156 | """ 157 | Returns a list of all elements in ``self``. 158 | 159 | Returns: 160 | A list of :class:`Geometry` instances 161 | 162 | MEOS Functions: 163 | geoset_values 164 | """ 165 | elems = geoset_values(self._inner) 166 | return [ 167 | gserialized_to_shapely_geometry(elems[i]) 168 | for i in range(self.num_elements()) 169 | ] 170 | 171 | def srid(self) -> int: 172 | """ 173 | Returns the SRID of ``self``. 174 | 175 | Returns: 176 | An integer 177 | 178 | MEOS Functions: 179 | geoset_srid 180 | """ 181 | return geoset_srid(self._inner) 182 | 183 | # ------------------------- Topological Operations -------------------------------- 184 | 185 | def contains(self, content: Union[GeoSet, str]) -> bool: 186 | """ 187 | Returns whether ``self`` contains ``content``. 188 | 189 | Args: 190 | content: object to compare with 191 | 192 | Returns: 193 | True if contains, False otherwise 194 | """ 195 | return super().contains(content) 196 | 197 | # ------------------------- Set Operations -------------------------------- 198 | 199 | @overload 200 | def intersection(self, other: shp.Geometry) -> Optional[shp.Geometry]: ... 201 | 202 | @overload 203 | def intersection(self, other: GeoSet) -> Optional[GeoSet]: ... 204 | 205 | def intersection(self, other): 206 | """ 207 | Returns the intersection of ``self`` and ``other``. 208 | 209 | Args: 210 | other: A :class:`GeoSet` or :class:`Geometry` instance 211 | 212 | Returns: 213 | An object of the same type as ``other`` or ``None`` if the intersection is empty. 214 | 215 | MEOS Functions: 216 | intersection_set_geo, intersection_set_set 217 | """ 218 | if isinstance(other, shp.Geometry): 219 | return gserialized_to_shapely_geometry( 220 | intersection_set_geo(self._inner, geometry_to_gserialized(other))[0] 221 | ) 222 | elif isinstance(other, GeoSet): 223 | result = intersection_set_set(self._inner, other._inner) 224 | return GeoSet(_inner=result) if result is not None else None 225 | else: 226 | return super().intersection(other) 227 | 228 | def minus(self, other: Union[GeoSet, shp.Geometry]) -> Optional[GeoSet]: 229 | """ 230 | Returns the difference of ``self`` and ``other``. 231 | 232 | Args: 233 | other: A :class:`GeoSet` or :class:`Geometry` instance 234 | 235 | Returns: 236 | A :class:`GeoSet` instance or ``None`` if the difference is empty. 237 | 238 | MEOS Functions: 239 | minus_set_geo, minus_set_set 240 | 241 | See Also: 242 | :meth:`subtract_from` 243 | """ 244 | if isinstance(other, shp.Geometry): 245 | result = minus_set_geo(self._inner, geometry_to_gserialized(other)) 246 | return GeoSet(_inner=result) if result is not None else None 247 | elif isinstance(other, GeoSet): 248 | result = minus_set_set(self._inner, other._inner) 249 | return GeoSet(_inner=result) if result is not None else None 250 | else: 251 | return super().minus(other) 252 | 253 | def subtract_from(self, other: shp.Geometry) -> Optional[shp.Geometry]: 254 | """ 255 | Returns the difference of ``other`` and ``self``. 256 | 257 | Args: 258 | other: A :class:`Geometry` instance 259 | 260 | Returns: 261 | A :class:`Geometry` instance. 262 | 263 | MEOS Functions: 264 | minus_geo_set 265 | 266 | See Also: 267 | :meth:`minus` 268 | """ 269 | result = minus_geo_set(geometry_to_gserialized(other), self._inner) 270 | return ( 271 | gserialized_to_shapely_geometry(result[0]) if result is not None else None 272 | ) 273 | 274 | def union(self, other: Union[GeoSet, shp.Geometry]) -> GeoSet: 275 | """ 276 | Returns the union of ``self`` and ``other``. 277 | 278 | Args: 279 | other: A :class:`GeoSet` or :class:`Geometry` instance 280 | 281 | Returns: 282 | A :class:`GeoSet` instance. 283 | 284 | MEOS Functions: 285 | union_set_geo, union_set_set 286 | """ 287 | if isinstance(other, shp.Geometry): 288 | result = union_set_geo(self._inner, geometry_to_gserialized(other)) 289 | return GeoSet(_inner=result) if result is not None else None 290 | elif isinstance(other, GeoSet): 291 | result = union_set_set(self._inner, other._inner) 292 | return GeoSet(_inner=result) if result is not None else None 293 | else: 294 | return super().union(other) 295 | 296 | # ------------------------- Transformations ------------------------------------ 297 | 298 | def round(self: Self, max_decimals: int) -> Self: 299 | """ 300 | Rounds the coordinate values to a number of decimal places. 301 | 302 | Args: 303 | max_decimals: The number of decimal places to use for the coordinates. 304 | 305 | Returns: 306 | A new :class:`GeoSet` object of the same subtype of ``self``. 307 | 308 | MEOS Functions: 309 | tpoint_roundgeoset_round 310 | """ 311 | return self.__class__(_inner=geoset_round(self._inner, max_decimals)) 312 | 313 | 314 | class GeometrySet(GeoSet): 315 | _mobilitydb_name = "geomset" 316 | 317 | _parse_function = geomset_in 318 | _parse_value_function = lambda x: ( 319 | pgis_geometry_in(x, -1) if isinstance(x, str) else geometry_to_gserialized(x) 320 | ) 321 | 322 | 323 | class GeographySet(GeoSet): 324 | _mobilitydb_name = "geogset" 325 | 326 | _parse_function = geogset_in 327 | _parse_value_function = lambda x: ( 328 | pgis_geography_in(x, -1) if isinstance(x, str) else geography_to_gserialized(x) 329 | ) 330 | -------------------------------------------------------------------------------- /pymeos/aggregators/aggregator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import abc 4 | from datetime import datetime, timedelta 5 | from typing import Optional, Union, List, Generic, TypeVar, Callable 6 | 7 | from pymeos_cffi import * 8 | 9 | from ..boxes import Box 10 | from ..factory import _TemporalFactory 11 | from ..temporal import Temporal 12 | from ..collections import Time 13 | 14 | ResultType = TypeVar("ResultType", bound=Union[Temporal, Time, Box]) 15 | SourceType = TypeVar("SourceType", bound=Union[Temporal, Time, Box]) 16 | StateType = TypeVar("StateType") 17 | SourceMeosType = TypeVar("SourceMeosType") 18 | ResultMeosType = TypeVar("ResultMeosType") 19 | SelfAgg = TypeVar("SelfAgg", bound="Aggregation") 20 | 21 | IntervalType = TypeVar("IntervalType") 22 | OriginType = TypeVar("OriginType") 23 | 24 | 25 | class BaseAggregator(Generic[SourceType, ResultType], abc.ABC): 26 | """ 27 | Abstract class for all aggregations. 28 | 29 | Can be easily extended overriding the :func:`~pymeos.aggregators.aggregator.BaseAggregator._add_function` and 30 | :func:`~pymeos.aggregators.aggregator.BaseAggregator._final_function` when the aggregation uses the inner MEOS 31 | objects, or :func:`~pymeos.aggregators.aggregator.BaseAggregator._add` and 32 | :func:`~pymeos.aggregators.aggregator.BaseAggregator._finish` for arbitrary aggregations. 33 | """ 34 | 35 | @staticmethod 36 | def _add_function( 37 | state: Optional[StateType], meos_object: SourceMeosType 38 | ) -> StateType: 39 | """ 40 | Add `meos_object` to the aggregation. Usually a MEOS function. 41 | Args: 42 | state: current state of the aggregation. 43 | meos_object: new MEOS object to aggregate. 44 | 45 | Returns: 46 | New state of the aggregation after adding `meos_object`. 47 | 48 | """ 49 | raise NotImplemented 50 | 51 | @staticmethod 52 | def _final_function(state: StateType) -> ResultMeosType: 53 | """ 54 | Return the current value of the aggregation. Usually a MEOS function. 55 | Args: 56 | state: current state of the aggregation. 57 | 58 | Returns: 59 | Value of the aggregation. 60 | 61 | """ 62 | return temporal_tagg_finalfn(state) 63 | 64 | @classmethod 65 | def _add(cls, state: Optional[StateType], temporal: SourceType) -> StateType: 66 | """ 67 | Add the `temporal` object to the aggregation. 68 | Args: 69 | state: current state of the aggregation. 70 | temporal: new PyMEOS object to aggregate. 71 | 72 | Returns: 73 | New state of the aggregation after adding `temporal`. 74 | 75 | """ 76 | return cls._add_function(state, temporal._inner) 77 | 78 | @classmethod 79 | def _finish(cls, state) -> ResultType: 80 | """ 81 | Return the current value of the aggregation. 82 | Args: 83 | state: current state of the aggregation. 84 | 85 | Returns: 86 | Value of the aggregation. 87 | 88 | """ 89 | result = cls._final_function(state) 90 | return _TemporalFactory.create_temporal(result) 91 | 92 | @classmethod 93 | def aggregate(cls, temporals: List[SourceType]) -> ResultType: 94 | """ 95 | Aggregate a list of PyMEOS object at once. 96 | 97 | Useful when only the final result is desired and all the objects are available. 98 | 99 | For aggregating in a streaming fashion, see 100 | :func:'pymeos.aggregators.aggregator.BaseAggregator.start_aggregation'. 101 | 102 | Args: 103 | temporals: list of PyMEOS objects to aggregate. 104 | 105 | Returns: 106 | Result of applying the aggregation to the passed objects. 107 | 108 | """ 109 | state = None 110 | for t in temporals: 111 | state = cls._add(state, t) 112 | return cls._finish(state) 113 | 114 | @classmethod 115 | def start_aggregation(cls) -> Aggregation: 116 | """ 117 | Return an :class:`Aggregation` object that holds the state of a new aggregation. 118 | 119 | Returns: 120 | An :class:`Aggregation` instance 121 | """ 122 | return Aggregation(cls._add, cls._finish) 123 | 124 | @classmethod 125 | def _error(cls, element): 126 | raise TypeError( 127 | f"Cannot perform aggregation ({cls.__name__}) with the following element: " 128 | f"{element} (Class: {element.__class__})" 129 | ) 130 | 131 | 132 | class Aggregation(Generic[SourceType, ResultType]): 133 | """ 134 | Class representing an aggregation in process. 135 | 136 | This class is returned by the :func:`~pymeos.aggregators.aggregator.BaseAggregator.start_aggregation` method 137 | of :class:`BaseAggregator` subclasses, and shouldn't be created directly by the final user. 138 | """ 139 | 140 | def __init__(self, add_function, finish_function) -> None: 141 | super().__init__() 142 | self._add_function = add_function 143 | self._finish_function = finish_function 144 | self._state = None 145 | 146 | def add(self: SelfAgg, new_temporal: SourceType) -> SelfAgg: 147 | """ 148 | Add a new element to the aggregation. 149 | 150 | Returns itself to allowing chaining 151 | 152 | Examples: 153 | >>> aggregation.add(temporal1).add(temporal2) 154 | Is equivalent to 155 | >>> aggregation.add(temporal1) 156 | >>> aggregation.add(temporal2) 157 | 158 | Args: 159 | new_temporal: PyMEOS object to add to the aggregation 160 | 161 | Returns: 162 | `self` 163 | """ 164 | self._state = self._add_function(self._state, new_temporal) 165 | return self 166 | 167 | def aggregation(self) -> ResultType: 168 | """ 169 | Return the current aggregation value. 170 | 171 | Note that this doesn't finish the aggregation, so more elements can be still added. 172 | 173 | Examples: 174 | >>> aggregation.add(temporal1).add(temporal2) 175 | >>> intermediate = aggregation.aggregation() 176 | >>> aggregation.add(temporal3).add(temporal4) 177 | >>> final_result = aggregation.aggregation() 178 | 179 | Returns: 180 | Result of the aggregation 181 | """ 182 | return self._finish_function(self._state) 183 | 184 | 185 | class BaseGranularAggregator(Generic[SourceType, ResultType, IntervalType, OriginType]): 186 | """ 187 | Abstract class for granular aggregations. 188 | """ 189 | 190 | @staticmethod 191 | def _add_function( 192 | state: Optional[StateType], 193 | meos_object: SourceMeosType, 194 | interval: IntervalType, 195 | origin: OriginType, 196 | ) -> StateType: 197 | """ 198 | Add `meos_object` to the aggregation. Usually a MEOS function. 199 | Args: 200 | state: current state of the aggregation. 201 | meos_object: new MEOS object to aggregate. 202 | interval: width of the aggregation intervals 203 | origin: starting value of the first interval 204 | 205 | Returns: 206 | New state of the aggregation after adding `meos_object`. 207 | 208 | """ 209 | raise NotImplemented 210 | 211 | @staticmethod 212 | def _final_function(state: StateType) -> ResultMeosType: 213 | """ 214 | Return the current value of the aggregation. Usually a MEOS function. 215 | Args: 216 | state: current state of the aggregation. 217 | 218 | Returns: 219 | Value of the aggregation. 220 | 221 | """ 222 | return temporal_tagg_finalfn(state) 223 | 224 | @classmethod 225 | def _add( 226 | cls, 227 | state: Optional[StateType], 228 | temporal: SourceType, 229 | interval: IntervalType, 230 | origin: OriginType, 231 | ) -> StateType: 232 | """ 233 | Add the `temporal` object to the aggregation. 234 | Args: 235 | state: current state of the aggregation. 236 | temporal: new PyMEOS object to aggregate. 237 | interval: width of the aggregation intervals 238 | origin: starting value of the first interval 239 | 240 | Returns: 241 | New state of the aggregation after adding `temporal`. 242 | 243 | """ 244 | interval_converted = ( 245 | timedelta_to_interval(interval) 246 | if isinstance(interval, timedelta) 247 | else pg_interval_in(interval, -1) if isinstance(interval, str) else None 248 | ) 249 | origin_converted = ( 250 | datetime_to_timestamptz(origin) 251 | if isinstance(origin, datetime) 252 | else pg_timestamptz_in(origin, -1) 253 | ) 254 | return cls._add_function( 255 | state, temporal._inner, interval_converted, origin_converted 256 | ) 257 | 258 | @classmethod 259 | def _finish(cls, state: StateType) -> ResultType: 260 | """ 261 | Return the current value of the aggregation. 262 | Args: 263 | state: current state of the aggregation. 264 | 265 | Returns: 266 | Value of the aggregation. 267 | 268 | """ 269 | result = cls._final_function(state) 270 | return _TemporalFactory.create_temporal(result) 271 | 272 | @classmethod 273 | def aggregate( 274 | cls, temporals: List[SourceType], interval: IntervalType, origin: OriginType 275 | ) -> ResultType: 276 | """ 277 | Aggregate a list of PyMEOS object at once with certain granularity. 278 | 279 | Useful when only the final result is desired and all the objects are available. 280 | 281 | For aggregating in a streaming fashion, see 282 | :func:`~pymeos.aggregators.aggregator.BaseGranularAggregator.start_aggregation`. 283 | 284 | Args: 285 | temporals: list of PyMEOS objects to aggregate. 286 | interval: width of the aggregation intervals 287 | origin: starting value of the first interval 288 | 289 | Returns: 290 | Result of applying the granular aggregation to the passed objects. 291 | 292 | """ 293 | state = None 294 | for t in temporals: 295 | state = cls._add(state, t, interval, origin) 296 | return cls._finish(state) 297 | 298 | @classmethod 299 | def start_aggregation( 300 | cls, interval: IntervalType, origin: OriginType 301 | ) -> GranularAggregation[SourceType, ResultType, IntervalType, OriginType]: 302 | return GranularAggregation(cls._add, cls._finish, interval, origin) 303 | 304 | @classmethod 305 | def _error(cls, element): 306 | raise TypeError( 307 | f"Cannot perform aggregation ({cls.__name__}) with the following element: " 308 | f"{element} (Class: {element.__class__})" 309 | ) 310 | 311 | 312 | class GranularAggregation( 313 | Aggregation[SourceType, ResultType], 314 | Generic[SourceType, ResultType, IntervalType, OriginType], 315 | ): 316 | """ 317 | Class representing a granular aggregation in process. 318 | 319 | This class is returned by the :func:`~pymeos.aggregators.aggregator.BaseGranularAggregator.start_aggregation` method 320 | of :class:`BaseGranularAggregator` subclasses, and shouldn't be created directly by the final user. 321 | """ 322 | 323 | def __init__( 324 | self, 325 | add_function: Callable[ 326 | [Optional[StateType], SourceType, IntervalType, OriginType], StateType 327 | ], 328 | finish_function: Callable[[StateType], ResultType], 329 | interval: IntervalType, 330 | origin: OriginType, 331 | ) -> None: 332 | super().__init__(add_function, finish_function) 333 | self._interval = interval 334 | self._origin = origin 335 | 336 | def add(self: SelfAgg, new_temporal: SourceType) -> SelfAgg: 337 | self._state = self._add_function( 338 | self._state, new_temporal, self._interval, self._origin 339 | ) 340 | return self 341 | -------------------------------------------------------------------------------- /tests/collections/time/tstzset_test.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from datetime import datetime, timezone, timedelta 3 | from typing import List 4 | 5 | import pytest 6 | 7 | from pymeos import ( 8 | TsTzSpan, 9 | TsTzSpanSet, 10 | TsTzSet, 11 | ) 12 | from tests.conftest import TestPyMEOS 13 | 14 | 15 | class TestTsTzSet(TestPyMEOS): 16 | ts_set = TsTzSet( 17 | "{2019-09-01 00:00:00+0, 2019-09-02 00:00:00+0, 2019-09-03 00:00:00+0}" 18 | ) 19 | 20 | @staticmethod 21 | def assert_tstzset_equality(ts_set: TsTzSet, timestamps: List[datetime]): 22 | assert ts_set.num_elements() == len(timestamps) 23 | assert ts_set.elements() == timestamps 24 | 25 | 26 | class TestTsTzSetConstructors(TestTsTzSet): 27 | def test_string_constructor(self): 28 | self.assert_tstzset_equality( 29 | self.ts_set, 30 | [ 31 | datetime(2019, 9, 1, 0, 0, 0, tzinfo=timezone.utc), 32 | datetime(2019, 9, 2, 0, 0, 0, tzinfo=timezone.utc), 33 | datetime(2019, 9, 3, 0, 0, 0, tzinfo=timezone.utc), 34 | ], 35 | ) 36 | 37 | def test_list_constructor(self): 38 | ts_set = TsTzSet( 39 | elements=[ 40 | datetime(2019, 9, 1, 0, 0, 0, tzinfo=timezone.utc), 41 | datetime(2019, 9, 2, 0, 0, 0, tzinfo=timezone.utc), 42 | datetime(2019, 9, 3, 0, 0, 0, tzinfo=timezone.utc), 43 | ] 44 | ) 45 | self.assert_tstzset_equality( 46 | ts_set, 47 | [ 48 | datetime(2019, 9, 1, 0, 0, 0, tzinfo=timezone.utc), 49 | datetime(2019, 9, 2, 0, 0, 0, tzinfo=timezone.utc), 50 | datetime(2019, 9, 3, 0, 0, 0, tzinfo=timezone.utc), 51 | ], 52 | ) 53 | 54 | def test_hexwkb_constructor(self): 55 | ts_set = TsTzSet.from_hexwkb( 56 | TsTzSet( 57 | elements=[ 58 | datetime(2019, 9, 1, 0, 0, 0, tzinfo=timezone.utc), 59 | datetime(2019, 9, 2, 0, 0, 0, tzinfo=timezone.utc), 60 | datetime(2019, 9, 3, 0, 0, 0, tzinfo=timezone.utc), 61 | ] 62 | ).as_hexwkb() 63 | ) 64 | self.assert_tstzset_equality( 65 | ts_set, 66 | [ 67 | datetime(2019, 9, 1, 0, 0, 0, tzinfo=timezone.utc), 68 | datetime(2019, 9, 2, 0, 0, 0, tzinfo=timezone.utc), 69 | datetime(2019, 9, 3, 0, 0, 0, tzinfo=timezone.utc), 70 | ], 71 | ) 72 | 73 | def test_from_as_constructor(self): 74 | assert self.ts_set == TsTzSet(str(self.ts_set)) 75 | assert self.ts_set == TsTzSet.from_wkb(self.ts_set.as_wkb()) 76 | assert self.ts_set == TsTzSet.from_hexwkb(self.ts_set.as_hexwkb()) 77 | 78 | def test_copy_constructor(self): 79 | ts_set_copy = copy(self.ts_set) 80 | assert self.ts_set == ts_set_copy 81 | assert self.ts_set is not ts_set_copy 82 | 83 | 84 | class TestTsTzSetOutputs(TestTsTzSet): 85 | def test_str(self): 86 | assert ( 87 | str(self.ts_set) 88 | == '{"2019-09-01 00:00:00+00", "2019-09-02 00:00:00+00", "2019-09-03 00:00:00+00"}' 89 | ) 90 | 91 | def test_repr(self): 92 | assert ( 93 | repr(self.ts_set) 94 | == 'TsTzSet({"2019-09-01 00:00:00+00", "2019-09-02 00:00:00+00", "2019-09-03 00:00:00+00"})' 95 | ) 96 | 97 | def test_as_hexwkb(self): 98 | assert self.ts_set == TsTzSet.from_hexwkb(self.ts_set.as_hexwkb()) 99 | 100 | 101 | class TestTimestampConversions(TestTsTzSet): 102 | def test_to_tstzspanset(self): 103 | assert self.ts_set.to_spanset() == TsTzSpanSet( 104 | "{[2019-09-01 00:00:00+00, 2019-09-01 00:00:00+00], " 105 | "[2019-09-02 00:00:00+00, 2019-09-02 00:00:00+00], " 106 | "[2019-09-03 00:00:00+00, 2019-09-03 00:00:00+00]}" 107 | ) 108 | 109 | 110 | class TestTsTzSetAccessors(TestTsTzSet): 111 | def test_duration(self): 112 | assert self.ts_set.duration() == timedelta(days=2) 113 | 114 | def test_tstzspan(self): 115 | assert self.ts_set.to_span() == TsTzSpan( 116 | "[2019-09-01 00:00:00+00, 2019-09-03 00:00:00+00]" 117 | ) 118 | 119 | def test_num_timestamps(self): 120 | assert self.ts_set.num_elements() == 3 121 | assert len(self.ts_set) == 3 122 | 123 | def test_start_timestamp(self): 124 | assert self.ts_set.start_element() == datetime( 125 | 2019, 9, 1, 0, 0, 0, tzinfo=timezone.utc 126 | ) 127 | 128 | def test_end_timestamp(self): 129 | assert self.ts_set.end_element() == datetime( 130 | 2019, 9, 3, 0, 0, 0, tzinfo=timezone.utc 131 | ) 132 | 133 | def test_timestamp_n(self): 134 | assert self.ts_set.element_n(1) == datetime( 135 | 2019, 9, 2, 0, 0, 0, tzinfo=timezone.utc 136 | ) 137 | 138 | def test_timestamp_n_out_of_range(self): 139 | with pytest.raises(IndexError): 140 | self.ts_set.element_n(3) 141 | 142 | def test_timestamps(self): 143 | assert self.ts_set.elements() == [ 144 | datetime(2019, 9, 1, 0, 0, 0, tzinfo=timezone.utc), 145 | datetime(2019, 9, 2, 0, 0, 0, tzinfo=timezone.utc), 146 | datetime(2019, 9, 3, 0, 0, 0, tzinfo=timezone.utc), 147 | ] 148 | 149 | def test_hash(self): 150 | assert hash(self.ts_set) == 527267058 151 | 152 | 153 | class TestTsTzSetPositionFunctions(TestTsTzSet): 154 | timestamp = datetime(year=2020, month=1, day=1) 155 | tstzset = TsTzSet("{2020-01-01 00:00:00+0, 2020-01-31 00:00:00+0}") 156 | 157 | @pytest.mark.parametrize("other, expected", [(tstzset, False)], ids=["tstzset"]) 158 | def test_is_contained_in(self, other, expected): 159 | assert self.ts_set.is_contained_in(other) == expected 160 | 161 | @pytest.mark.parametrize( 162 | "other", [timestamp, tstzset], ids=["timestamp", "tstzset"] 163 | ) 164 | def test_contains(self, other): 165 | self.ts_set.contains(other) 166 | _ = other in self.tstzset 167 | 168 | @pytest.mark.parametrize("other", [tstzset], ids=["tstzset"]) 169 | def test_overlaps(self, other): 170 | self.ts_set.overlaps(other) 171 | 172 | @pytest.mark.parametrize( 173 | "other", [timestamp, tstzset], ids=["timestamp", "tstzset"] 174 | ) 175 | def test_is_before(self, other): 176 | self.ts_set.is_before(other) 177 | 178 | @pytest.mark.parametrize( 179 | "other", [timestamp, tstzset], ids=["timestamp", "tstzset"] 180 | ) 181 | def test_is_over_or_before(self, other): 182 | self.ts_set.is_over_or_before(other) 183 | 184 | @pytest.mark.parametrize( 185 | "other", [timestamp, tstzset], ids=["timestamp", "tstzset"] 186 | ) 187 | def test_is_after(self, other): 188 | self.ts_set.is_after(other) 189 | 190 | @pytest.mark.parametrize( 191 | "other", [timestamp, tstzset], ids=["timestamp", "tstzset"] 192 | ) 193 | def test_is_over_or_after(self, other): 194 | self.ts_set.is_over_or_after(other) 195 | 196 | @pytest.mark.parametrize( 197 | "other", [timestamp, tstzset], ids=["timestamp", "tstzset"] 198 | ) 199 | def test_distance(self, other): 200 | self.ts_set.distance(other) 201 | 202 | 203 | class TestTsTzSetSetFunctions(TestTsTzSet): 204 | timestamp = datetime(year=2020, month=1, day=1) 205 | tstzset = TsTzSet("{2020-01-01 00:00:00+0, 2020-01-31 00:00:00+0}") 206 | tstzspan = TsTzSpan("(2020-01-01 00:00:00+0, 2020-01-31 00:00:00+0)") 207 | tstzspanset = TsTzSpanSet( 208 | "{(2020-01-01 00:00:00+0, 2020-01-31 00:00:00+0), (2021-01-01 00:00:00+0, 2021-01-31 00:00:00+0)}" 209 | ) 210 | 211 | @pytest.mark.parametrize( 212 | "other", 213 | [tstzspan, tstzspanset, timestamp, tstzset], 214 | ids=["tstzspan", "tstzspanset", "timestamp", "tstzset"], 215 | ) 216 | def test_intersection(self, other): 217 | self.tstzset.intersection(other) 218 | self.tstzset * other 219 | 220 | @pytest.mark.parametrize( 221 | "other", 222 | [tstzspan, tstzspanset, timestamp, tstzset], 223 | ids=["tstzspan", "tstzspanset", "timestamp", "tstzset"], 224 | ) 225 | def test_union(self, other): 226 | self.tstzset.union(other) 227 | self.tstzset + other 228 | 229 | @pytest.mark.parametrize( 230 | "other", 231 | [tstzspan, tstzspanset, timestamp, tstzset], 232 | ids=["tstzspan", "tstzspanset", "timestamp", "tstzset"], 233 | ) 234 | def test_minus(self, other): 235 | self.tstzset.minus(other) 236 | self.tstzset - other 237 | 238 | 239 | class TestTsTzSetComparisons(TestTsTzSet): 240 | tstzset = TsTzSet("{2020-01-01 00:00:00+0, 2020-01-31 00:00:00+0}") 241 | other = TsTzSet("{2020-01-02 00:00:00+0, 2020-03-31 00:00:00+0}") 242 | 243 | def test_eq(self): 244 | _ = self.tstzset == self.other 245 | 246 | def test_ne(self): 247 | _ = self.tstzset != self.other 248 | 249 | def test_lt(self): 250 | _ = self.tstzset < self.other 251 | 252 | def test_le(self): 253 | _ = self.tstzset <= self.other 254 | 255 | def test_gt(self): 256 | _ = self.tstzset > self.other 257 | 258 | def test_ge(self): 259 | _ = self.tstzset >= self.other 260 | 261 | 262 | class TestTsTzSetFunctionsFunctions(TestTsTzSet): 263 | tstzset = TsTzSet( 264 | "{2020-01-01 00:00:00+0, 2020-01-02 00:00:00+0, 2020-01-04 00:00:00+0}" 265 | ) 266 | 267 | @pytest.mark.parametrize( 268 | "delta,result", 269 | [ 270 | ( 271 | timedelta(days=4), 272 | [ 273 | datetime(2020, 1, 5, tzinfo=timezone.utc), 274 | datetime(2020, 1, 6, tzinfo=timezone.utc), 275 | datetime(2020, 1, 8, tzinfo=timezone.utc), 276 | ], 277 | ), 278 | ( 279 | timedelta(days=-4), 280 | [ 281 | datetime(2019, 12, 28, tzinfo=timezone.utc), 282 | datetime(2019, 12, 29, tzinfo=timezone.utc), 283 | datetime(2019, 12, 31, tzinfo=timezone.utc), 284 | ], 285 | ), 286 | ( 287 | timedelta(hours=2), 288 | [ 289 | datetime(2020, 1, 1, 2, tzinfo=timezone.utc), 290 | datetime(2020, 1, 2, 2, tzinfo=timezone.utc), 291 | datetime(2020, 1, 4, 2, tzinfo=timezone.utc), 292 | ], 293 | ), 294 | ( 295 | timedelta(hours=-2), 296 | [ 297 | datetime(2019, 12, 31, 22, tzinfo=timezone.utc), 298 | datetime(2020, 1, 1, 22, tzinfo=timezone.utc), 299 | datetime(2020, 1, 3, 22, tzinfo=timezone.utc), 300 | ], 301 | ), 302 | ], 303 | ids=["positive days", "negative days", "positive hours", "negative hours"], 304 | ) 305 | def test_shift(self, delta, result): 306 | shifted = self.tstzset.shift(delta) 307 | self.assert_tstzset_equality(shifted, result) 308 | 309 | @pytest.mark.parametrize( 310 | "delta,result", 311 | [ 312 | ( 313 | timedelta(days=6), 314 | [ 315 | datetime(2020, 1, 1, tzinfo=timezone.utc), 316 | datetime(2020, 1, 3, tzinfo=timezone.utc), 317 | datetime(2020, 1, 7, tzinfo=timezone.utc), 318 | ], 319 | ), 320 | ( 321 | timedelta(hours=3), 322 | [ 323 | datetime(2020, 1, 1, tzinfo=timezone.utc), 324 | datetime(2020, 1, 1, 1, tzinfo=timezone.utc), 325 | datetime(2020, 1, 1, 3, tzinfo=timezone.utc), 326 | ], 327 | ), 328 | ], 329 | ids=["days", "hours"], 330 | ) 331 | def test_scale(self, delta, result): 332 | scaled = self.tstzset.scale(delta) 333 | self.assert_tstzset_equality(scaled, result) 334 | 335 | def test_shift_scale(self): 336 | shifted_scaled = self.tstzset.shift_scale(timedelta(days=4), timedelta(hours=3)) 337 | self.assert_tstzset_equality( 338 | shifted_scaled, 339 | [ 340 | datetime(2020, 1, 5, tzinfo=timezone.utc), 341 | datetime(2020, 1, 5, 1, tzinfo=timezone.utc), 342 | datetime(2020, 1, 5, 3, tzinfo=timezone.utc), 343 | ], 344 | ) 345 | 346 | 347 | class TestTsTzSetMiscFunctions(TestTsTzSet): 348 | tstzset = TsTzSet( 349 | "{2020-01-01 00:00:00+0, 2020-01-02 00:00:00+0, 2020-01-04 00:00:00+0}" 350 | ) 351 | 352 | def test_hash(self): 353 | hash(self.tstzset) 354 | -------------------------------------------------------------------------------- /pymeos/collections/number/intset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List, Union, overload, Optional, TYPE_CHECKING 4 | 5 | from pymeos_cffi import ( 6 | intset_in, 7 | intset_make, 8 | intset_out, 9 | intset_start_value, 10 | intset_end_value, 11 | intset_value_n, 12 | intset_values, 13 | contains_set_int, 14 | intersection_set_int, 15 | intersection_set_set, 16 | minus_set_int, 17 | left_set_int, 18 | overleft_set_int, 19 | right_set_int, 20 | overright_set_int, 21 | minus_set_set, 22 | union_set_set, 23 | union_set_int, 24 | intset_shift_scale, 25 | minus_int_set, 26 | distance_set_int, 27 | distance_intset_intset, 28 | ) 29 | 30 | from .intspan import IntSpan 31 | from .intspanset import IntSpanSet 32 | from ..base import Set 33 | 34 | if TYPE_CHECKING: 35 | from .floatset import FloatSet 36 | 37 | 38 | class IntSet(Set[int]): 39 | """ 40 | Class for representing a set of integer values. 41 | 42 | ``IntSet`` objects can be created with a single argument of type string as 43 | in MobilityDB. 44 | 45 | >>> IntSet(string='{1, 3, 56}') 46 | 47 | Another possibility is to create a ``IntSet`` object from a list of 48 | strings or integers. 49 | 50 | >>> IntSet(elements=[1, '2', 3, '56']) 51 | 52 | """ 53 | 54 | __slots__ = ["_inner"] 55 | 56 | _mobilitydb_name = "intset" 57 | 58 | _parse_function = intset_in 59 | _parse_value_function = int 60 | _make_function = intset_make 61 | 62 | # ------------------------- Constructors ---------------------------------- 63 | 64 | # ------------------------- Output ---------------------------------------- 65 | 66 | def __str__(self): 67 | """ 68 | Return the string representation of the content of ``self``. 69 | 70 | Returns: 71 | A new :class:`str` instance 72 | 73 | MEOS Functions: 74 | intset_out 75 | """ 76 | return intset_out(self._inner) 77 | 78 | # ------------------------- Conversions ----------------------------------- 79 | 80 | def to_floatset(self) -> FloatSet: 81 | """ 82 | Converts ``self`` to a :class:`FloatSet` instance. 83 | 84 | Returns: 85 | A new :class:`FloatSet` instance 86 | """ 87 | from .floatset import FloatSet 88 | 89 | return FloatSet(elements=[float(x) for x in self.elements()]) 90 | 91 | # ------------------------- Accessors ------------------------------------- 92 | 93 | def start_element(self) -> int: 94 | """ 95 | Returns the first element in ``self``. 96 | 97 | Returns: 98 | A :class:`int` instance 99 | 100 | MEOS Functions: 101 | intset_start_value 102 | """ 103 | return intset_start_value(self._inner) 104 | 105 | def end_element(self) -> int: 106 | """ 107 | Returns the last element in ``self``. 108 | 109 | Returns: 110 | A :class:`int` instance 111 | 112 | MEOS Functions: 113 | intset_end_value 114 | """ 115 | return intset_end_value(self._inner) 116 | 117 | def element_n(self, n: int) -> int: 118 | """ 119 | Returns the ``n``-th element in ``self``. 120 | 121 | Args: 122 | n: The 0-based index of the element to return. 123 | 124 | Returns: 125 | A :class:`int` instance 126 | 127 | MEOS Functions: 128 | intset_value_n 129 | """ 130 | super().element_n(n) 131 | return intset_value_n(self._inner, n + 1) 132 | 133 | def elements(self) -> List[int]: 134 | """ 135 | Returns the elements in ``self``. 136 | 137 | Returns: 138 | A list of :class:`int` instances 139 | 140 | MEOS Functions: 141 | inttset_values 142 | """ 143 | elems = intset_values(self._inner) 144 | return [elems[i] for i in range(self.num_elements())] 145 | 146 | # ------------------------- Transformations ------------------------------------ 147 | 148 | def shift(self, delta: int) -> IntSet: 149 | """ 150 | Returns a new ``IntSet`` instance with all elements shifted by ``delta``. 151 | 152 | Args: 153 | delta: The value to shift by. 154 | 155 | Returns: 156 | A new :class:`IntSet` instance 157 | 158 | MEOS Functions: 159 | intset_shift_scale 160 | """ 161 | return self.shift_scale(delta, None) 162 | 163 | def scale(self, width: int) -> IntSet: 164 | """ 165 | Returns a new ``IntSet`` instance with all elements scaled to so that the encompassing 166 | span has width ``width``. 167 | 168 | Args: 169 | width: The new width. 170 | 171 | Returns: 172 | A new :class:`IntSet` instance 173 | 174 | MEOS Functions: 175 | intset_shift_scale 176 | """ 177 | return self.shift_scale(None, width) 178 | 179 | def shift_scale(self, delta: Optional[int], width: Optional[int]) -> IntSet: 180 | """ 181 | Returns a new ``IntSet`` instance with all elements shifted by 182 | ``delta`` and scaled to so that the encompassing span has width 183 | ``width``. 184 | 185 | Args: 186 | delta: The value to shift by. 187 | width: The new width. 188 | 189 | Returns: 190 | A new :class:`IntSet` instance 191 | 192 | MEOS Functions: 193 | intset_shift_scale 194 | """ 195 | return IntSet( 196 | _inner=intset_shift_scale( 197 | self._inner, delta, width, delta is not None, width is not None 198 | ) 199 | ) 200 | 201 | # ------------------------- Topological Operations -------------------------------- 202 | 203 | def contains(self, content: Union[IntSet, int]) -> bool: 204 | """ 205 | Returns whether ``self`` contains ``content``. 206 | 207 | Args: 208 | content: object to compare with 209 | 210 | Returns: 211 | True if contains, False otherwise 212 | 213 | MEOS Functions: 214 | contains_set_set, contains_set_int 215 | """ 216 | if isinstance(content, int): 217 | return contains_set_int(self._inner, content) 218 | else: 219 | return super().contains(content) 220 | 221 | # ------------------------- Position Operations -------------------------------- 222 | 223 | def is_left(self, content: Union[IntSet, int]) -> bool: 224 | """ 225 | Returns whether ``self`` is strictly to the left of ``other``. That is, 226 | ``self`` ends before ``other`` starts. 227 | 228 | Args: 229 | content: object to compare with 230 | 231 | Returns: 232 | True if left, False otherwise 233 | 234 | MEOS Functions: 235 | left_set_set, left_set_int 236 | """ 237 | if isinstance(content, int): 238 | return left_set_int(self._inner, content) 239 | else: 240 | return super().is_left(content) 241 | 242 | def is_over_or_left(self, content: Union[IntSet, int]) -> bool: 243 | """ 244 | Returns whether ``self`` is to the left of ``other`` allowing overlap. 245 | That is, ``self`` ends before ``other`` ends (or at the same value). 246 | 247 | Args: 248 | content: object to compare with 249 | 250 | Returns: 251 | True if is over or left, False otherwise 252 | 253 | MEOS Functions: 254 | overleft_set_set, overleft_set_int 255 | """ 256 | if isinstance(content, int): 257 | return overleft_set_int(self._inner, content) 258 | else: 259 | return super().is_over_or_left(content) 260 | 261 | def is_right(self, content: Union[IntSet, int]) -> bool: 262 | """ 263 | Returns whether ``self`` is strictly to the right of ``other``. That is, 264 | ``self`` ends after ``other`` starts. 265 | 266 | Args: 267 | content: object to compare with 268 | 269 | Returns: 270 | True if right, False otherwise 271 | 272 | MEOS Functions: 273 | right_set_set, right_set_int 274 | """ 275 | if isinstance(content, int): 276 | return right_set_int(self._inner, content) 277 | else: 278 | return super().is_right(content) 279 | 280 | def is_over_or_right(self, content: Union[IntSet, int]) -> bool: 281 | """ 282 | Returns whether ``self`` is to the right of ``other`` allowing overlap. 283 | That is, ``self`` starts before ``other`` ends (or at the same value). 284 | 285 | Args: 286 | content: object to compare with 287 | 288 | Returns: 289 | True if is over or right, False otherwise 290 | 291 | MEOS Functions: 292 | overright_set_set, overright_set_int 293 | """ 294 | if isinstance(content, int): 295 | return overright_set_int(self._inner, content) 296 | else: 297 | return super().is_over_or_right(content) 298 | 299 | # ------------------------- Set Operations -------------------------------- 300 | 301 | @overload 302 | def intersection(self, other: int) -> Optional[int]: ... 303 | 304 | @overload 305 | def intersection(self, other: IntSet) -> Optional[IntSet]: ... 306 | 307 | def intersection(self, other): 308 | """ 309 | Returns the intersection of ``self`` and ``other``. 310 | 311 | Args: 312 | other: A :class:`IntSet` or :class:`int` instance 313 | 314 | Returns: 315 | An object of the same type as ``other`` or ``None`` if the 316 | intersection is empty. 317 | 318 | MEOS Functions: 319 | intersection_set_set, intersection_set_int 320 | """ 321 | if isinstance(other, int): 322 | return intersection_set_int(self._inner, other) 323 | elif isinstance(other, IntSet): 324 | result = intersection_set_set(self._inner, other._inner) 325 | return IntSet(_inner=result) if result is not None else None 326 | else: 327 | return super().intersection(other) 328 | 329 | def minus(self, other: Union[IntSet, int]) -> Optional[IntSet]: 330 | """ 331 | Returns the difference of ``self`` and ``other``. 332 | 333 | Args: 334 | other: A :class:`IntSet` or :class:`int` instance 335 | 336 | Returns: 337 | A :class:`IntSet` instance or ``None`` if the difference is empty. 338 | 339 | MEOS Functions: 340 | minus_set_set, minus_set_int 341 | """ 342 | if isinstance(other, int): 343 | result = minus_set_int(self._inner, other) 344 | return IntSet(_inner=result) if result is not None else None 345 | elif isinstance(other, IntSet): 346 | result = minus_set_set(self._inner, other._inner) 347 | return IntSet(_inner=result) if result is not None else None 348 | else: 349 | return super().minus(other) 350 | 351 | def subtract_from(self, other: int) -> Optional[int]: 352 | """ 353 | Returns the difference of ``other`` and ``self``. 354 | 355 | Args: 356 | other: A :class:`int` instance 357 | 358 | Returns: 359 | A :class:`int` instance or ``None`` if the difference is empty. 360 | 361 | MEOS Functions: 362 | minus_int_set 363 | 364 | See Also: 365 | :meth:`minus` 366 | """ 367 | return minus_int_set(other, self._inner) 368 | 369 | def union(self, other: Union[IntSet, int]) -> IntSet: 370 | """ 371 | Returns the union of ``self`` and ``other``. 372 | 373 | Args: 374 | other: A :class:`IntSet` or :class:`int` instance 375 | 376 | Returns: 377 | A :class:`IntSet` instance. 378 | 379 | MEOS Functions: 380 | union_set_set, union_set_int 381 | """ 382 | if isinstance(other, int): 383 | result = union_set_int(self._inner, other) 384 | return IntSet(_inner=result) if result is not None else None 385 | elif isinstance(other, IntSet): 386 | result = union_set_set(self._inner, other._inner) 387 | return IntSet(_inner=result) if result is not None else None 388 | else: 389 | return super().union(other) 390 | 391 | # ------------------------- Distance Operations --------------------------- 392 | 393 | def distance(self, other: Union[int, IntSet, IntSpan, IntSpanSet]) -> int: 394 | """ 395 | Returns the distance between ``self`` and ``other``. 396 | 397 | Args: 398 | other: object to compare with 399 | 400 | Returns: 401 | A :class:`int` instance 402 | 403 | MEOS Functions: 404 | distance_set_int, distance_intset_intset, distance_intspanset_intspan, 405 | distance_intspanset_intspanset 406 | """ 407 | from .intspan import IntSpan 408 | from .intspanset import IntSpanSet 409 | 410 | if isinstance(other, int): 411 | return distance_set_int(self._inner, other) 412 | elif isinstance(other, IntSet): 413 | return distance_intset_intset(self._inner, other._inner) 414 | elif isinstance(other, IntSpan): 415 | return self.to_spanset().distance(other) 416 | elif isinstance(other, IntSpanSet): 417 | return self.to_spanset().distance(other) 418 | else: 419 | return super().distance(other) 420 | --------------------------------------------------------------------------------