diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index d1c864f..fea3ebc 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -38,21 +38,21 @@ jobs: - name: Test run: coverage run -m pytest - - name: Post coverage results - uses: coverallsapp/github-action@v2 - with: - flag-name: run-${{ matrix.python-version }} - parallel: true - - finish: - needs: test - if: ${{ always() }} - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: coverallsapp/github-action@v2 - with: - parallel-finished: true + # - name: Post coverage results + # uses: coverallsapp/github-action@v2 + # with: + # flag-name: run-${{ matrix.python-version }} + # parallel: true + + # finish: + # needs: test + # if: ${{ always() }} + # runs-on: ubuntu-latest + # steps: + # - name: Coveralls Finished + # uses: coverallsapp/github-action@v2 + # with: + # parallel-finished: true lint: name: Formatting check diff --git a/docs/conf.py b/docs/conf.py index faa0f36..13c05ba 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,7 @@ "sphinx.ext.mathjax", "sphinx_autodoc_typehints", "matplotlib.sphinxext.plot_directive", + "enum_tools.autoenum", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/index.rst b/docs/index.rst index 84d0083..b8bc5a2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,4 +14,4 @@ Indices and tables tree/getting_started.rst - + tree/reference.rst diff --git a/docs/tree/figures/settlement_rod.png b/docs/tree/figures/settlement_rod.png new file mode 100644 index 0000000..b5bea10 Binary files /dev/null and b/docs/tree/figures/settlement_rod.png differ diff --git a/docs/tree/reference.rst b/docs/tree/reference.rst new file mode 100644 index 0000000..32f6231 --- /dev/null +++ b/docs/tree/reference.rst @@ -0,0 +1,76 @@ +.. _reference: + +Reference +========= + +.. _settlementRodMeasurement: + +Settlement Rod Measurement +---------------------------- + +A settlement rod device consists of a rod and a (bottom) settlement plate (see figure below). The measurements are taken at the top of the rod (i.e. `rod_top`) +and at the `ground surface` and since the `rod length` is known, the settlement of at the bottom of the rod (i.e. `rod_bottom`) can be also derived. + +.. image:: figures/settlement_rod.png + +The class `SettlementRodMeasurement` presented below stores the data of a single settlement rod measurement. + +.. autoclass:: baec.measurements.settlement_rod_measurement.SettlementRodMeasurement + :members: + :inherited-members: + :member-order: bysource + + .. automethod:: __init__ + +.. autoenum:: baec.measurements.settlement_rod_measurement.SettlementRodMeasurementStatus + :members: + + +.. _settlementRodMeasurementSeries: + +Settlement Rod Measurement Series +--------------------------------- + +.. autoclass:: baec.measurements.settlement_rod_measurement_series.SettlementRodMeasurementSeries + :members: + :inherited-members: + :member-order: bysource + + .. automethod:: __init__ + + +.. _measurementDevice: + +Measurement Device +------------------ + +.. autoclass:: baec.measurements.measurement_device.MeasurementDevice + :members: + :inherited-members: + :member-order: bysource + + .. automethod:: __init__ + +.. _project: + +Project +------- + +.. autoclass:: baec.project.Project + :members: + :inherited-members: + :member-order: bysource + + .. automethod:: __init__ + +CoordinateReferenceSystems +-------------------------- + +.. _coordinateReferenceSystems: + +.. autoclass:: baec.coordinates.CoordinateReferenceSystems + :members: + :inherited-members: + :member-order: bysource + + .. automethod:: __init__ diff --git a/pyproject.toml b/pyproject.toml index eced000..bdca795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ 'cems-nuclei[client]>=0.5,<1', 'matplotlib>=3.8,<4', "tqdm[notebook]>4,<5", + "pyproj>3,<4", ] license = { file = "LICENSE" } readme = "README.md" @@ -28,7 +29,8 @@ docs = [ "sphinx-autodoc-typehints==1.22", "ipython==8.11.0", "asteroid-sphinx-theme==0.0.3", - "sphinx_rtd_theme==1.2.0", + "sphinx_rtd_theme>1.2,<2", + "enum-tools[sphinx]>0.12,<0.13", ] # lint dependencies from github super-linter v5 # See https://github.com/super-linter/super-linter/tree/main/dependencies/python @@ -62,7 +64,7 @@ ensure_newline_before_comments = true line_length = 88 [tool.mypy] -files = ["src/pypilecore"] +files = ["src/baec"] mypy_path = 'src' namespace_packages = true show_error_codes = true @@ -84,10 +86,8 @@ module = [ "matplotlib.*", "requests.*", "nuclei.*", - "pygef.*", - "natsort.*", - "shapely.*", "pytest.*", "scipy.*", + "pyproj.*", ] ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt index 8d295e4..4ba8d2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,24 +6,38 @@ # alabaster==0.7.16 # via sphinx +apeye==1.4.1 + # via sphinx-toolbox +apeye-core==1.1.5 + # via apeye asteroid-sphinx-theme==0.0.3 # via baec (pyproject.toml) asttokens==2.4.1 # via stack-data +autodocsumm==0.2.12 + # via sphinx-toolbox babel==2.15.0 # via sphinx backcall==0.2.0 # via ipython +beautifulsoup4==4.12.3 + # via sphinx-toolbox black[jupyter]==23.10.1 # via # baec (pyproject.toml) # black +cachecontrol[filecache]==0.14.0 + # via + # cachecontrol + # sphinx-toolbox cems-nuclei[client]==0.5.5 # via # baec (pyproject.toml) # cems-nuclei -certifi==2024.2.2 - # via requests +certifi==2024.6.2 + # via + # pyproj + # requests charset-normalizer==3.3.2 # via requests click==8.1.3 @@ -40,26 +54,49 @@ coverage[toml]==7.5.3 # coveralls coveralls==4.0.1 # via baec (pyproject.toml) +cssutils==2.11.1 + # via dict2css cycler==0.12.1 # via matplotlib decorator==5.1.1 # via ipython +dict2css==0.3.0.post1 + # via sphinx-toolbox docopt==0.6.2 # via coveralls docutils==0.18.1 # via # sphinx + # sphinx-prompt # sphinx-rtd-theme + # sphinx-tabs + # sphinx-toolbox +domdf-python-tools==3.8.1 + # via + # apeye + # apeye-core + # dict2css + # sphinx-toolbox +enum-tools[sphinx]==0.12.0 + # via baec (pyproject.toml) exceptiongroup==1.2.1 # via pytest executing==2.0.1 # via stack-data +filelock==3.14.0 + # via + # cachecontrol + # sphinx-toolbox flake8==6.0.0 # via baec (pyproject.toml) -fonttools==4.52.4 +fonttools==4.53.0 # via matplotlib +html5lib==1.1 + # via sphinx-toolbox idna==3.7 - # via requests + # via + # apeye-core + # requests imagesize==1.4.1 # via sphinx importlib-metadata==7.1.0 @@ -81,13 +118,17 @@ isort==5.12.0 jedi==0.19.1 # via ipython jinja2==3.1.4 - # via sphinx + # via + # sphinx + # sphinx-jinja2-compat jupyterlab-widgets==3.0.11 # via ipywidgets kiwisolver==1.4.5 # via matplotlib markupsafe==2.1.5 - # via jinja2 + # via + # jinja2 + # sphinx-jinja2-compat matplotlib==3.9.0 # via baec (pyproject.toml) matplotlib-inline==0.1.7 @@ -96,6 +137,10 @@ mccabe==0.7.0 # via # baec (pyproject.toml) # flake8 +more-itertools==10.2.0 + # via cssutils +msgpack==1.0.8 + # via cachecontrol mypy==1.6.1 # via baec (pyproject.toml) mypy-extensions==1.0.0 @@ -103,6 +148,8 @@ mypy-extensions==1.0.0 # baec (pyproject.toml) # black # mypy +natsort==8.4.0 + # via domdf-python-tools numpy==1.26.4 # via # baec (pyproject.toml) @@ -138,6 +185,7 @@ pillow==10.3.0 # via matplotlib platformdirs==3.5.1 # via + # apeye # baec (pyproject.toml) # black pluggy==1.5.0 @@ -158,12 +206,17 @@ pyflakes==3.0.1 # flake8 pygments==2.18.0 # via + # enum-tools # ipython # sphinx + # sphinx-prompt + # sphinx-tabs pyjwt==2.6.0 # via cems-nuclei pyparsing==3.1.2 # via matplotlib +pyproj==3.6.1 + # via baec (pyproject.toml) pytest==8.2.1 # via baec (pyproject.toml) python-dateutil==2.9.0.post0 @@ -174,26 +227,52 @@ pytz==2024.1 # via pandas requests==2.32.3 # via + # apeye + # cachecontrol # cems-nuclei # coveralls # sphinx +ruamel-yaml==0.18.6 + # via sphinx-toolbox +ruamel-yaml-clib==0.2.8 + # via ruamel-yaml six==1.16.0 # via # asttokens + # html5lib # python-dateutil snowballstemmer==2.2.0 # via sphinx +soupsieve==2.5 + # via beautifulsoup4 sphinx==6.1.3 # via # asteroid-sphinx-theme + # autodocsumm # baec (pyproject.toml) + # enum-tools # sphinx-autodoc-typehints + # sphinx-prompt # sphinx-rtd-theme + # sphinx-tabs + # sphinx-toolbox # sphinxcontrib-jquery sphinx-autodoc-typehints==1.22 + # via + # baec (pyproject.toml) + # sphinx-toolbox +sphinx-jinja2-compat==0.2.0.post1 + # via + # enum-tools + # sphinx-toolbox +sphinx-prompt==1.6.0 + # via sphinx-toolbox +sphinx-rtd-theme==2.0.0 # via baec (pyproject.toml) -sphinx-rtd-theme==1.2.0 - # via baec (pyproject.toml) +sphinx-tabs==3.4.5 + # via sphinx-toolbox +sphinx-toolbox==3.5.0 + # via enum-tools sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -210,6 +289,8 @@ sphinxcontrib-serializinghtml==1.1.10 # via sphinx stack-data==0.6.3 # via ipython +tabulate==0.9.0 + # via sphinx-toolbox tokenize-rt==5.2.0 # via black tomli==2.0.1 @@ -237,16 +318,21 @@ typing-extensions==4.7.1 # via # baec (pyproject.toml) # black + # domdf-python-tools + # enum-tools # mypy + # sphinx-toolbox tzdata==2024.1 # via pandas urllib3==2.2.1 # via requests wcwidth==0.2.13 # via prompt-toolkit +webencodings==0.5.1 + # via html5lib widgetsnbextension==4.0.11 # via ipywidgets -zipp==3.19.0 +zipp==3.19.1 # via # importlib-metadata # importlib-resources diff --git a/src/baec/coordinates.py b/src/baec/coordinates.py new file mode 100644 index 0000000..1c5441c --- /dev/null +++ b/src/baec/coordinates.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +from functools import cached_property + +import pyproj + + +class CoordinateReferenceSystems: + """ + Represents the horizontal (X, Y) and vertical (Z) coordinate reference systems of a 3D point. + """ + + def __init__(self, horizontal: pyproj.CRS, vertical: pyproj.CRS) -> None: + """ + Initializes a CoordinateReferenceSystems object. + + Parameters + ---------- + horizontal : pyproj.CRS + The coordinate reference system of the X and Y-coordinates. + It is a `pyproj.CRS` object (see https://pyproj4.github.io/pyproj/stable/api/crs/crs.html) of + type 'Projected' or 'Compound'. + vertical : pyproj.CRS + The coordinate reference system of the Z-coordinate. + It is a `pyproj.CRS` object (see https://pyproj4.github.io/pyproj/stable/api/crs/crs.html) of + type 'Vertical' or 'Compound'. + + Raises + ------ + TypeError + If the input types are incorrect. + ValueError + If `horizontal` is not a projected or compound CRS. + If `vertical` is not a vertical or compound CRS. + """ + + # Initialize all attributes using private setters. + self._set_horizontal(horizontal) + self._set_vertical(vertical) + + @classmethod + def from_epsg(cls, horizontal: int, vertical: int) -> CoordinateReferenceSystems: + """ + Creates a CoordinateReferenceSystems object from the EPSG codes of the horizontal and vertical CRS. + + Note + ---- + If your settlement rod is located in the Netherlands the horizontal coordinate reference systems is likely `28992` + (Amersfoort / RD New) and the vertical `5709` (NAP height). To combine use `7415` (Amersfoort / RD New + NAP height). + + Parameters + ---------- + horizontal : int + The EPSG code of the horizontal CRS. + vertical : int + The EPSG code of the vertical CRS. + + Returns + ------- + CoordinateReferenceSystems + A CoordinateReferenceSystems object with the horizontal and vertical CRS. + + Raises + ------ + pyproj.exceptions.CRSError + If the EPSG codes are not valid. + """ + return cls( + horizontal=pyproj.CRS.from_epsg(horizontal), + vertical=pyproj.CRS.from_epsg(vertical), + ) + + def _set_horizontal(self, value: pyproj.CRS) -> None: + """ + Private setter for horizontal attribute. + """ + if not isinstance(value, pyproj.CRS): + raise TypeError("Expected 'pyproj.CRS' type for 'horizontal' attribute.") + + # Check whether the CRS is a projected or compound CRS. + if not value.is_projected and not value.is_compound: + raise ValueError( + "Expected 'is_projected' or 'is_compound' to be true for 'horizontal' attribute." + ) + + # Set the coordinate system in case of a projected CRS. + if value.is_projected: + self._horizontal = value + # Set the coordinate system in case of a compound CRS. + elif value.is_compound: + projected_crs = None + for crs in value.sub_crs_list: + if crs.is_projected: + projected_crs = crs + break + if projected_crs is None: + raise ValueError("No projected CRS found in the compound CRS.") + self._horizontal = projected_crs + + def _set_vertical(self, value: pyproj.CRS) -> None: + """ + Private setter for z attribute. + """ + if not isinstance(value, pyproj.CRS): + raise TypeError("Expected 'pyproj.CRS' type for 'vertical' attribute.") + + # Check whether the CRS is a vertical or compound CRS. + if not value.is_vertical and not value.is_compound: + raise ValueError( + "Expected 'is_vertical' or 'is_compound' to be true for 'vertical' attribute." + ) + + # Set the coordinate system in case of a vertical CRS. + if value.is_vertical: + self._vertical = value + # Set the coordinate system in case of a compound CRS. + elif value.is_compound: + vertical_crs = None + for crs in value.sub_crs_list: + if crs.is_vertical: + vertical_crs = crs + break + if vertical_crs is None: + raise ValueError("No vertical CRS found in the compound CRS.") + self._vertical = vertical_crs + + @property + def horizontal(self) -> pyproj.CRS: + """ + The coordinate reference system of the horizontal X and Y-coordinates. + """ + return self._horizontal + + @property + def vertical(self) -> pyproj.CRS: + """ + The coordinate reference system of the vertical Z-coordinate. + """ + return self._vertical + + @property + def horizontal_units(self) -> str: + """ + The units of the horizontal CRS. + """ + return self._horizontal.axis_info[0].unit_name + + @property + def vertical_units(self) -> str: + """ + The units of the vertical CRS + """ + return self._vertical.axis_info[0].unit_name + + @property + def vertical_datum(self) -> str: + """ + The name of the vertical datum. + """ + return self._vertical.name + + @cached_property + def vertical_datum_and_units(self) -> str: + """ + The vertical datum and units of the vertical CRS. + """ + return f"{self.vertical_datum} [{self.vertical_units}]" + + def __str__(self) -> str: + """Converts the object to a string.""" + return f"CoordinateReferenceSystems(Horizontal: {self.horizontal.to_epsg()}, Vertical: {self.vertical.to_epsg()})" + + def __eq__(self, value: object) -> bool: + """Compares two CoordinateReferenceSystems objects.""" + if not isinstance(value, CoordinateReferenceSystems): + return False + return self.horizontal.equals(value.horizontal) and self.vertical.equals( + value.vertical + ) diff --git a/src/baec/measurements/__init__.py b/src/baec/measurements/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/baec/measurements/measurement_device.py b/src/baec/measurements/measurement_device.py new file mode 100644 index 0000000..930c770 --- /dev/null +++ b/src/baec/measurements/measurement_device.py @@ -0,0 +1,90 @@ +from __future__ import annotations + + +class MeasurementDevice: + """ + Represents a measurement device. + """ + + def __init__( + self, + id_: str, + qr_code: str | None = None, + ) -> None: + """ + Initializes a MeasurementDevice object. + + Parameters + ---------- + id_ : str + The ID of the measurement device. + qr_code : str | None, optional + The QR code of the measurement device, or None if unknown (default: None). + + Raises + ------ + TypeError + If the input types are incorrect. + ValueError + If empty string for `id_` or `qr_code`. + """ + + # Initialize all attributes using private setters. + self._set_id(id_) + self._set_qr_code(qr_code) + + def _set_id(self, value: str) -> None: + """ + Private setter for id attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'id' attribute.") + if value == "": + raise ValueError("Empty string not allowed for 'id' attribute.") + self._id = value + + def _set_qr_code(self, value: str | None) -> None: + """ + Private setter for qr_code attribute. + """ + if value is not None: + if not isinstance(value, str): + raise TypeError( + "Expected 'str' or 'None' type for 'qr_code' attribute." + ) + if value == "": + raise ValueError("Empty string not allowed for 'qr_code' attribute.") + self._qr_code = value + + @property + def id(self) -> str: + """ + The ID of the measurement device. + """ + return self._id + + @property + def qr_code(self) -> str | None: + """ + The QR-code of the measurement device. + """ + return self._qr_code + + def __eq__(self, other: object) -> bool: + """ + Check if two MeasurementDevice objects are equal. + It compares the `id` attribute. + + Parameters + ---------- + other : object + The object to compare. + + Returns + ------- + bool + True if the objects are equal, False otherwise. + """ + if not isinstance(other, MeasurementDevice): + return False + return self.id == other.id diff --git a/src/baec/measurements/settlement_rod_measurement.py b/src/baec/measurements/settlement_rod_measurement.py new file mode 100644 index 0000000..77b9afa --- /dev/null +++ b/src/baec/measurements/settlement_rod_measurement.py @@ -0,0 +1,431 @@ +from __future__ import annotations + +import datetime +from enum import Enum +from functools import cache + +from baec.coordinates import CoordinateReferenceSystems +from baec.measurements.measurement_device import MeasurementDevice +from baec.project import Project + + +class SettlementRodMeasurementStatus(Enum): + """Represents the status of a settlement rod measurement.""" + + OK = "ok" + DISTURBED = "disturbed" + EXPIRED = "expired" + RELOCATED = "relocated" + ROD_IS_EXTENDED = "rod_is_extended" + CROOKED = "crooked" + DESELECTED = "deselected" + FICTIONAL = "fictional" + UNKNOWN = "unknown" + + +class SettlementRodMeasurement: + """ + Represents a single settlement rod measurement. + """ + + def __init__( + self, + project: Project, + device: MeasurementDevice, + object_id: str, + date_time: datetime.datetime, + coordinate_reference_systems: CoordinateReferenceSystems, + rod_top_x: float, + rod_top_y: float, + rod_top_z: float, + rod_length: float, + rod_bottom_z: float, + ground_surface_z: float, + status: SettlementRodMeasurementStatus, + temperature: float | None = None, + voltage: float | None = None, + comment: str = "", + ) -> None: + """ + Initializes a SettlementRodMeasurement object. + + Parameters + ---------- + project : Project + The project which the measurement belongs to. + device : MeasurementDevice + The measurement device. + date_time : datetime.datetime + The date and time of the measurement. + object_id : str + The ID of the measured object. + date_time : datetime.datetime + The date and time of the measurement. + coordinate_reference_systems : CoordinateReferenceSystems + The horizontal (X, Y) and vertical (Z) coordinate reference systems (CRS) of the + spatial measurements. + rod_top_x : float + The horizontal X-coordinate of the top of the settlement rod. + Units are according to the `coordinate_reference_systems`. + rod_top_y : float + The horizontal Y-coordinate of the top of the settlement rod. + Units are according to the `coordinate_reference_systems`. + rod_top_z : float + The vertical Z-coordinate of the top of the settlement rod. + It is the top of the settlement rod. + Units and datum are according to the `coordinate_reference_systems`. + rod_length : float + The length of the settlement rod including the thickness of the settlement plate. + It is in principle the vertical distance between the top of the settlement rod and + the bottom of the settlement plate. + Units are according to the `coordinate_reference_systems`. + rod_bottom_z : float + The corrected Z-coordinate at the bottom of the settlement rod (coincides with bottom of settlement plate). + Note that the bottom of the plate is in principle the original ground surface. + Units and datum according to the `coordinate_reference_systems`. + ground_surface_z : float + The Z-coordinate of the ground surface. + It is in principle the top of the fill, if present. + Units and datum according to the `coordinate_reference_systems`. + status: SettlementRodMeasurementStatus + The status of the measurement. + temperature : float or None, optional + The temperature at the time of measurement in [°C], or None if unknown (default: None). + voltage : float or None, optional + The voltage measured in [mV], or None if unknown (default: None). + comment : str, optional + Additional comment about the measurement (default: ""). + + Raises + ------ + TypeError + If the input types are incorrect. + ValueError + If empty string for `object_id`. + If negative value for `rod_length`. + """ + + # Initialize all attributes using private setters. + self._set_project(project) + self._set_device(device) + self._set_object_id(object_id) + self._set_date_time(date_time) + self._set_coordinate_reference_systems(coordinate_reference_systems) + self._set_rod_top_x(rod_top_x) + self._set_rod_top_y(rod_top_y) + self._set_rod_top_z(rod_top_z) + self._set_rod_length(rod_length) + self._set_rod_bottom_z(rod_bottom_z) + self._set_ground_surface_z(ground_surface_z) + self._set_status(status) + self._set_temperature(temperature) + self._set_voltage(voltage) + self._set_comment(comment) + + def _set_project(self, value: Project) -> None: + """ + Private setter for project attribute. + """ + if not isinstance(value, Project): + raise TypeError("Expected 'Project' type for 'project' attribute.") + self._project = value + + def _set_device(self, value: MeasurementDevice) -> None: + """ + Private setter for device attribute. + """ + if not isinstance(value, MeasurementDevice): + raise TypeError("Expected 'MeasurementDevice' type for 'device' attribute.") + self._device = value + + def _set_object_id(self, value: str) -> None: + """ + Private setter for object_id attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'object_id' attribute.") + if value == "": + raise ValueError("Empty string not allowed for 'object_id' attribute.") + self._object_id = value + + def _set_date_time(self, value: datetime.datetime) -> None: + """ + Private setter for date_time attribute. + """ + if not isinstance(value, datetime.datetime): + raise TypeError( + "Expected 'datetime.datetime' type for 'date_time' attribute." + ) + self._date_time = value + + def _set_coordinate_reference_systems( + self, value: CoordinateReferenceSystems + ) -> None: + """ + Private setter for coordinate_reference_systems attribute. + """ + if not isinstance(value, CoordinateReferenceSystems): + raise TypeError( + "Expected 'CoordinateReferenceSystems' type for 'coordinate_reference_systems' attribute." + ) + self._coordinate_reference_systems = value + + def _set_rod_top_x(self, value: float) -> None: + """ + Private setter for rod_top_x attribute. + """ + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError("Expected 'float' type for 'rod_top_x' attribute.") + self._x = value + + def _set_rod_top_y(self, value: float) -> None: + """ + Private setter for rod_top_y attribute. + """ + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError("Expected 'float' type 'rod_top_y' attribute.") + self._y = value + + def _set_rod_top_z(self, value: float) -> None: + """ + Private setter for rod_top_z attribute. + """ + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError("Expected 'float' type for 'rod_top_z' attribute.") + self._z = value + + def _set_rod_length(self, value: float) -> None: + """ + Private setter for rod_length attribute. + """ + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError("Expected 'float' type for 'rod_length' attribute.") + if value < 0: + raise ValueError("Negative value not allowed for 'rod_length' attribute.") + self._rod_length = value + + def _set_rod_bottom_z(self, value: float) -> None: + """ + Private setter for rod_bottom_z attribute. + """ + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError("Expected 'float' type for 'rod_bottom_z' attribute.") + self._rod_bottom_z = value + + def _set_ground_surface_z(self, value: float) -> None: + """ + Private setter for ground_surface_z attribute. + """ + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError("Expected 'float' type for 'ground_surface_z' attribute.") + self._ground_surface_z = value + + def _set_status(self, value: SettlementRodMeasurementStatus) -> None: + """ + Private setter for status attribute. + """ + if not isinstance(value, SettlementRodMeasurementStatus): + raise TypeError( + "Expected 'SettlementRodMeasurementStatus' type for 'status' attribute." + ) + self._status = value + + def _set_temperature(self, value: float | None) -> None: + """ + Private setter for temperature attribute. + """ + if value is not None: + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError( + "Expected 'float' or 'None' type for 'temperature' attribute." + ) + self._temperature = value + + def _set_voltage(self, value: float | None) -> None: + """ + Private setter for voltage attribute. + """ + if value is not None: + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError( + "Expected 'float' or 'None' type for 'voltage' attribute." + ) + self._voltage = value + + def _set_comment(self, value: str) -> None: + """ + Private setter for comment attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'comment' attribute.") + self._comment = value + + @property + def project(self) -> Project: + """ + The project which the measurement belongs to. + """ + return self._project + + @property + def device(self) -> MeasurementDevice: + """ + The measurement device. + """ + return self._device + + @property + def object_id(self) -> str: + """ + The ID of the measured object. + """ + return self._object_id + + @property + def date_time(self) -> datetime.datetime: + """ + The date and time of the measurement. + """ + return self._date_time + + @property + def coordinate_reference_systems(self) -> CoordinateReferenceSystems: + """ + The horizontal (X, Y) and vertical (Z) coordinate reference systems (CRS) of the + spatial measurements. + """ + return self._coordinate_reference_systems + + @property + def rod_top_x(self) -> float: + """ + The horizontal X-coordinate of the top of the settlement rod. + Units are according to the `coordinate_reference_system`. + """ + return self._x + + @property + def rod_top_y(self) -> float: + """ + The horizontal Y-coordinate of the top of the settlement rod. + Units are according to the `coordinate_reference_system`. + """ + return self._y + + @property + def rod_top_z(self) -> float: + """ + The vertical Z-coordinate of the top of the settlement rod. + It is the top of the settlement rod. + Units are according to the `coordinate_reference_system`. + """ + return self._z + + @property + def rod_length(self) -> float: + """ + The length of the settlement rod including the thickness of the settlement plate. + It is in principle the vertical distance between the top of the settlement rod and the bottom of the settlement plate. + Units are according to the `coordinate_reference_system`. + """ + return self._rod_length + + @property + def rod_bottom_z(self) -> float: + """ + The corrected Z-coordinate at the bottom of the settlement rod (coincides with bottom of settlement plate). + Note that the bottom of the plate is in principle the original ground surface. + Units are according to the `coordinate_reference_system`. + """ + return self._rod_bottom_z + + @property + def rod_bottom_z_uncorrected(self) -> float: + """ + The uncorrected Z-coordinate at the bottom of the settlement rod (coincides with bottom of settlement plate). + It is computed as the difference beteen the Z-coordinate of the top of the settlement rod and the rod length. + Units are according to the `coordinate_reference_system`. + """ + return self.rod_top_z - self.rod_length + + @property + def ground_surface_z(self) -> float: + """ + The Z-coordinate of the ground surface. + It is in principle the top of the fill, if present. + """ + return self._ground_surface_z + + @property + def status(self) -> SettlementRodMeasurementStatus: + """ + The status of the measurement. + """ + return self._status + + @property + def temperature(self) -> float | None: + """ + The temperature at the time of measurement in [°C], or None if unknown. + """ + return self._temperature + + @property + def voltage(self) -> float | None: + """ + The voltage measured in [mV], or None if unknown. + """ + return self._voltage + + @property + def comment(self) -> str: + """ + Additional comment about the measurement. + """ + return self._comment + + @cache + def to_dict(self) -> dict: + """ + Convert the measurement to a dictionary. + """ + return { + "project_id": self.project.id, + "project_name": self.project.name, + "device_id": self.device.id, + "device_qr_code": self.device.qr_code, + "object_id": self.object_id, + "coordinate_horizontal_epsg_code": self.coordinate_reference_systems.horizontal.to_epsg(), + "coordinate_vertical_epsg_code": self.coordinate_reference_systems.vertical.to_epsg(), + "coordinate_horizontal_units": self.coordinate_reference_systems.horizontal_units, + "coordinate_vertical_units": self.coordinate_reference_systems.vertical_units, + "coordinate_vertical_datum": self.coordinate_reference_systems.vertical_datum, + "date_time": self.date_time, + "rod_top_x": self.rod_top_x, + "rod_top_y": self.rod_top_y, + "rod_top_z": self.rod_top_z, + "rod_length": self.rod_length, + "rod_bottom_z": self.rod_bottom_z, + "rod_bottom_z_uncorrected": self.rod_bottom_z_uncorrected, + "ground_surface_z": self.ground_surface_z, + "status": self.status.value, + "temperature": self.temperature, + "voltage": self.voltage, + "comment": self.comment, + } diff --git a/src/baec/measurements/settlement_rod_measurement_series.py b/src/baec/measurements/settlement_rod_measurement_series.py new file mode 100644 index 0000000..4916256 --- /dev/null +++ b/src/baec/measurements/settlement_rod_measurement_series.py @@ -0,0 +1,354 @@ +from __future__ import annotations + +from functools import cache +from typing import List + +import pandas as pd +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from matplotlib.figure import Figure + +from baec.coordinates import CoordinateReferenceSystems +from baec.measurements.measurement_device import MeasurementDevice +from baec.measurements.settlement_rod_measurement import SettlementRodMeasurement +from baec.project import Project + + +class SettlementRodMeasurementSeries: + """ + Represents a series of measurements for a single settlement rod. + """ + + def __init__(self, measurements: List[SettlementRodMeasurement]) -> None: + """ + Initializes a SettlementRodMeasurementSeries object. + + Parameters + ---------- + measurements : List[SettlementRodMeasurement] + The list of measurements for the settlement rod. + + Raises + ------ + TypeError + If the input types are incorrect. + ValueError + If the list of measurements is empty. + If the measurements are not for the same project, device, object or + coordinate refence systems. + """ + + # Initialize all attributes using private setters. + self._set_measurements(measurements) + + def _set_measurements(self, value: List[SettlementRodMeasurement]) -> None: + """Private setter for measurements attribute.""" + + # Check if the input is a list of SettlementRodMeasurement objects. + if not all(isinstance(item, SettlementRodMeasurement) for item in value): + raise TypeError( + "Expected 'List[SettlementRodMeasurement]' type for 'measurements' attribute." + ) + + # Check if the list is not empty. + if not value: + raise ValueError("Empty list not allowed for 'measurements' attribute.") + + # Check that the measurements are for the same project. + projects = [] + for measurement in value: + if measurement.project not in projects: + projects.append(measurement.project) + if len(projects) > 1: + raise ValueError( + "All measurements must be for the same project. " + + f"The following projects are found: {projects}" + ) + + # Check that the measurements are for the same device. + measurement_devices = [] + for measurement in value: + if measurement.device not in measurement_devices: + measurement_devices.append(measurement.device) + if len(measurement_devices) > 1: + raise ValueError( + "All measurements must be for the same device. " + + f"The following devices are found: {measurement_devices}" + ) + + # Check that the measurements are for the same object. + object_ids = [] + for measurement in value: + if measurement.object_id not in object_ids: + object_ids.append(measurement.object_id) + if len(object_ids) > 1: + raise ValueError( + "All measurements must be for the same measured object. " + + f"The following object IDs are found: {object_ids}" + ) + + # Check that the measurements are all in the same coordinate reference systems. + crs_list = [] + for measurement in value: + if measurement.coordinate_reference_systems not in crs_list: + crs_list.append(measurement.coordinate_reference_systems) + if len(crs_list) > 1: + raise ValueError( + "All measurements must be in the same coordinate reference systems. " + + f"The following object IDs are found: {crs_list}" + ) + + # Organize the list of measurements in chronological order. + self._measurements = sorted(value, key=lambda x: x.date_time) + + @property + def measurements(self) -> List[SettlementRodMeasurement]: + """ + The list of measurements for the settlement rod. + They are organized in chronological order. + """ + return self._measurements + + @property + def project(self) -> Project: + """ + The project which all the measurements belongs to. + """ + return self._measurements[0].project + + @property + def device(self) -> MeasurementDevice: + """ + The measurement device. + """ + return self._measurements[0].device + + @property + def object_id(self) -> str: + """ + The ID of the measured object. + """ + return self._measurements[0].object_id + + @property + def coordinate_reference_systems(self) -> CoordinateReferenceSystems: + """ + The horizontal (X, Y) and vertical (Z) coordinate reference systems of the measurements. + """ + return self._measurements[0].coordinate_reference_systems + + @cache + def to_dataframe(self) -> pd.DataFrame: + """ + Convert the series of measurements to a pandas DataFrame. + + Returns + ------- + pd.DataFrame + A pandas DataFrame with the measurements. The columns of the DataFrame are: + project_id, project_name, device_id, device_qr_code, object_id, + coordinate_horizontal_epsg_code, coordinate_vertical_epsg_code, + date_time, rod_top_x, rod_top_y, rod_top_z, rod_length, rod_bottom_z + rod_bottom_z_uncorrected, ground_surface_z, status, temperature, + voltage, comment. + """ + return pd.DataFrame.from_records( + [measurement.to_dict() for measurement in self.measurements] + ) + + def plot_x_time(self, axes: Axes | None = None) -> Axes: + """ + Plot the horizontal X-coordinates at the top of the rod over time. + + Parameters + ---------- + axes: plt.Axes + Axes to create the figure + + Returns + ------- + plt.Axes + """ + if axes is not None: + if not isinstance(axes, Axes): + raise TypeError("Expected 'Axes' type or None for 'axes' parameter.") + + if axes is None: + axes = plt.gca() + + df = self.to_dataframe() + + df.plot( + x="date_time", + y="rod_top_x", + ax=axes, + legend=True, + ) + + axes.set_ylim(df["rod_top_x"].min() - 0.5, df["rod_top_x"].max() + 0.5) + axes.grid() + + axes.set_ylabel(f"X [{self.coordinate_reference_systems.horizontal_units}]") + axes.set_xlabel("Date and Time") + axes.set_title(f"Horizontal X measurements for object: {self.object_id}") + + return axes + + def plot_y_time(self, axes: Axes | None = None) -> Axes: + """ + Plot the horizontal Y-coordinates at the top of the rod over time. + + Parameters + ---------- + axes: plt.Axes + Axes to create the figure + + Returns + ------- + plt.Axes + """ + if axes is not None: + if not isinstance(axes, Axes): + raise TypeError("Expected 'Axes' type or None for 'axes' parameter.") + + if axes is None: + axes = plt.gca() + + df = self.to_dataframe() + + df.plot( + x="date_time", + y="rod_top_y", + ax=axes, + legend=True, + ) + + axes.set_ylim(df["rod_top_y"].min() - 0.5, df["rod_top_y"].max() + 0.5) + axes.grid() + + axes.set_ylabel(f"Y [{self.coordinate_reference_systems.horizontal_units}]") + axes.set_xlabel("Date and Time") + axes.set_title(f"Horizontal Y measurements for object: {self.object_id}") + + return axes + + def plot_z_time(self, axes: Axes | None = None) -> Axes: + """ + Plot the vertical Z-coordinates at the top the rod, the ground surface and the bottom of the + rod over time. + + Parameters + ---------- + axes: plt.Axes + Axes to create the figure + + Returns + ------- + plt.Axes + """ + if axes is not None: + if not isinstance(axes, Axes): + raise TypeError("Expected 'Axes' type or None for 'axes' parameter.") + + if axes is None: + axes = plt.gca() + + df = self.to_dataframe() + z_cols = ["rod_top_z", "ground_surface_z", "rod_bottom_z"] + + df.plot( + x="date_time", + y=z_cols, + ax=axes, + legend=True, + ) + + axes.set_ylim(df[z_cols].values.min() - 0.5, df[z_cols].values.max() + 0.5) + axes.grid() + + axes.set_ylabel(self.coordinate_reference_systems.vertical_datum_and_units) + axes.set_xlabel("Date and Time") + axes.set_title(f"Vertical Z measurements for object: {self.object_id}") + + return axes + + def plot_xyz_time(self) -> Figure: + """ + Plot in a new figure the horizontal XY-coordinates at the top of rod and the + vertical Z-coordinates at the top the rod, the ground surface and the bottom of the + rod over time. + + Returns + ------- + plt.Figure + """ + fig, axes = plt.subplots(3, 1, figsize=(10, 30), sharex=True) + + fig.suptitle(f"Spatial measurements for object: {self.object_id}") + + self.plot_x_time(axes[0]) + axes[0].set_title("Horizontal X") + + self.plot_y_time(axes[1]) + axes[1].set_title("Horizontal Y") + + self.plot_z_time(axes[2]) + axes[2].set_title("Vertical Z") + + return fig + + def plot_xy_plan_view(self, axes: Axes | None = None) -> Axes: + """ + Plot the plan view of the horizontal XY coordinates + at the top of the rod. + + Parameters + ---------- + axes: plt.Axes + Axes to create the figure + + Returns + ------- + plt.Axes + """ + if axes is not None: + if not isinstance(axes, Axes): + raise TypeError("Expected 'Axes' type or None for 'axes' parameter.") + + if axes is None: + axes = plt.gca() + + df = self.to_dataframe() + + axes.plot(df["rod_top_x"], df["rod_top_y"]) + + # Mark the start and end of the measurements. + axes.plot( + df["rod_top_x"].iloc[0], + df["rod_top_y"].iloc[0], + marker="*", + color="black", + label="start", + ) + + axes.plot( + df["rod_top_x"].iloc[-1], + df["rod_top_y"].iloc[-1], + marker="+", + color="red", + label="end", + ) + + axes.legend(loc="upper right") + + axes.set_xlim(df["rod_top_x"].min() - 0.5, df["rod_top_x"].max() + 0.5) + axes.set_ylim(df["rod_top_y"].min() - 0.5, df["rod_top_y"].max() + 0.5) + axes.grid() + + axes.set_xlabel(f"X [{self.coordinate_reference_systems.horizontal_units}]") + axes.set_ylabel(f"Y [{self.coordinate_reference_systems.horizontal_units}]") + axes.set_title( + f"Plan view of horizonal measurements at rod top for object: {self.object_id}" + ) + + return axes diff --git a/src/baec/project.py b/src/baec/project.py new file mode 100644 index 0000000..0336834 --- /dev/null +++ b/src/baec/project.py @@ -0,0 +1,87 @@ +from __future__ import annotations + + +class Project: + """ + Represents a project. + """ + + def __init__( + self, + id_: str, + name: str, + ) -> None: + """ + Initializes a MeasurementDevice object. + + Parameters + ---------- + id_ : str + The ID of the project. + name : str + The name of the project. + + Raises + ------ + TypeError + If the input types are incorrect. + ValueError + If empty string for `id_` or `name`. + """ + + # Initialize all attributes using private setters. + self._set_id(id_) + self._set_name(name) + + def _set_id(self, value: str) -> None: + """ + Private setter for id attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'id' attribute.") + if value == "": + raise ValueError("Empty string not allowed for 'id' attribute.") + self._id = value + + def _set_name(self, value: str) -> None: + """ + Private setter for name attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'name' attribute.") + if value == "": + raise ValueError("Empty string not allowed for 'name' attribute.") + self._name = value + + @property + def id(self) -> str: + """ + The ID of the project. + """ + return self._id + + @property + def name(self) -> str: + """ + The name of the project. + """ + return self._name + + def __eq__(self, other: object) -> bool: + """ + Check if two MeasurementDevice objects are equal. + It compares the `id` attribute and `name` attribute. + + Parameters + ---------- + other : object + The object to compare. + + Returns + ------- + bool + True if the objects are equal, False otherwise. + """ + if not isinstance(other, Project): + return False + return self.id == other.id and self.name == other.name diff --git a/tests/measurements/conftest.py b/tests/measurements/conftest.py new file mode 100644 index 0000000..c599c24 --- /dev/null +++ b/tests/measurements/conftest.py @@ -0,0 +1,59 @@ +import datetime +from typing import List + +import pytest + +from baec.coordinates import CoordinateReferenceSystems +from baec.measurements.measurement_device import MeasurementDevice +from baec.measurements.settlement_rod_measurement import ( + SettlementRodMeasurement, + SettlementRodMeasurementStatus, +) +from baec.project import Project + + +@pytest.fixture +def example_settlement_rod_measurements() -> List[SettlementRodMeasurement]: + project = Project(id_="P-001", name="Project 1") + device = MeasurementDevice(id_="BR_003", qr_code="QR-003") + object_id = "ZB-02" + date_time_start = datetime.datetime(2024, 4, 9, 4, 0, 0) + coordinate_reference_systems = CoordinateReferenceSystems.from_epsg(28992, 5709) + rod_top_x_start = 123340.266 + rod_top_y_start = 487597.154 + rod_top_z_start = 0.807 + rod_length = 2.0 + plate_bottom_z = -1.193 + ground_surface_z = 0.419 + status = SettlementRodMeasurementStatus.OK + temperature = 12.0 + voltage = 4232 + comment = "No comment" + + measurements = [] + for i in range(10): + rod_top_x = rod_top_x_start + 0.05 * i + rod_top_y = rod_top_y_start - 0.03 * i + rod_top_z = rod_top_z_start - 0.01 * i + date_time = date_time_start + datetime.timedelta(days=i) + measurements.append( + SettlementRodMeasurement( + project=project, + device=device, + object_id=object_id, + date_time=date_time, + coordinate_reference_systems=coordinate_reference_systems, + rod_top_x=rod_top_x, + rod_top_y=rod_top_y, + rod_top_z=rod_top_z, + rod_length=rod_length, + rod_bottom_z=plate_bottom_z, + ground_surface_z=ground_surface_z, + status=status, + temperature=temperature, + voltage=voltage, + comment=comment, + ) + ) + + return measurements diff --git a/tests/measurements/test_measurement_device.py b/tests/measurements/test_measurement_device.py new file mode 100644 index 0000000..9c4d85c --- /dev/null +++ b/tests/measurements/test_measurement_device.py @@ -0,0 +1,55 @@ +import pytest + +from baec.measurements.measurement_device import MeasurementDevice + + +def test_measurement_device_with_valid_input() -> None: + """Test initialization of MeasurementDevice with valid input.""" + # With QR code + device = MeasurementDevice(id_="device_1", qr_code="qr_code_1") + assert device.id == "device_1" + assert device.qr_code == "qr_code_1" + + # Without QR code + device = MeasurementDevice(id_="device_1") + assert device.id == "device_1" + assert device.qr_code is None + + +def test_measurement_device_init_with_invalid_id() -> None: + """Test initialization of MeasurementDevice with invalid ID.""" + # Invalid id: None + with pytest.raises(TypeError, match="id"): + MeasurementDevice(id_=None) + + # Invalid id: Empty string + with pytest.raises(ValueError, match="id"): + MeasurementDevice(id_="") + + +def test_measurement_device_init_with_invalid_qr_code() -> None: + """Test initialization of MeasurementDevice with invalid QR-code.""" + # Invalid id: Integer value + with pytest.raises(TypeError, match="qr_code"): + MeasurementDevice(id_="device_1", qr_code=1) + + # Invalid id: Empty string + with pytest.raises(ValueError, match="qr_code"): + MeasurementDevice(id_="device_1", qr_code="") + + +def test_measurement_device__eq__method() -> None: + """Test the __eq__ method of MeasurementDevice.""" + device_1 = MeasurementDevice(id_="device_1", qr_code="qr_code_1") + device_2 = MeasurementDevice(id_="device_1", qr_code="qr_code_1") + device_3 = MeasurementDevice(id_="device_2", qr_code="qr_code_2") + device_4 = MeasurementDevice(id_="device_2", qr_code="qr_code_1") + + assert device_1 == device_2 + assert device_1 != device_3 + assert device_1 != device_4 + assert device_2 != device_3 + assert device_3 == device_4 + + assert device_1 == device_1 + assert device_1 != "device_1" diff --git a/tests/measurements/test_settlement_rod_measurement.py b/tests/measurements/test_settlement_rod_measurement.py new file mode 100644 index 0000000..9ce951c --- /dev/null +++ b/tests/measurements/test_settlement_rod_measurement.py @@ -0,0 +1,461 @@ +import datetime + +import pyproj +import pytest + +from baec.coordinates import CoordinateReferenceSystems +from baec.measurements.measurement_device import MeasurementDevice +from baec.measurements.settlement_rod_measurement import ( + SettlementRodMeasurement, + SettlementRodMeasurementStatus, +) +from baec.project import Project + + +def test_settlement_rod_measurement_init_with_valid_input() -> None: + """Test initialization of settlement rod measurement with valid input.""" + project = Project(id_="P-001", name="Project 1") + device = MeasurementDevice(id_="BR_003", qr_code="QR-003") + object_id = "ZB-02" + date_time = datetime.datetime(2024, 4, 9, 4, 0, 0) + coordinate_reference_systems = CoordinateReferenceSystems.from_epsg(28992, 5709) + rod_top_x = 123340.266 + rod_top_y = 487597.154 + rod_top_z = 0.807 + rod_length = 2.0 + rod_bottom_z = -1.193 + ground_surface_z = 0.419 + status = SettlementRodMeasurementStatus.OK + temperature = 12.0 + voltage = 4232 + comment = "No comment" + plate_bottom_z_uncorrected = -1.193 + + measurement = SettlementRodMeasurement( + project=project, + device=device, + object_id=object_id, + date_time=date_time, + coordinate_reference_systems=coordinate_reference_systems, + rod_top_x=rod_top_x, + rod_top_y=rod_top_y, + rod_top_z=rod_top_z, + rod_length=rod_length, + rod_bottom_z=rod_bottom_z, + ground_surface_z=ground_surface_z, + status=status, + temperature=temperature, + voltage=voltage, + comment=comment, + ) + + assert measurement.project == project + assert measurement.device == device + assert measurement.object_id == object_id + assert measurement.date_time == date_time + assert measurement.coordinate_reference_systems == coordinate_reference_systems + assert measurement.rod_top_x == rod_top_x + assert measurement.rod_top_y == rod_top_y + assert measurement.rod_top_z == rod_top_z + assert measurement.rod_length == rod_length + assert measurement.ground_surface_z == ground_surface_z + assert measurement.rod_bottom_z == rod_bottom_z + assert measurement.status == status + assert measurement.rod_bottom_z_uncorrected == plate_bottom_z_uncorrected + assert measurement.temperature == temperature + assert measurement.voltage == voltage + assert measurement.comment == comment + + +def test_settlement_rod_measurement_init_with_invalid_project() -> None: + """Test initialization of settlement rod measurement with invalid project.""" + # Invalid project: None + with pytest.raises(TypeError, match="project"): + SettlementRodMeasurement( + project=None, + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_device() -> None: + """Test initialization of settlement rod measurement with invalid device.""" + # Invalid device: None + with pytest.raises(TypeError, match="device"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=None, + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_object_id() -> None: + """Test initialization of settlement rod measurement with invalid object_id.""" + # Invalid point_id: None + with pytest.raises(TypeError, match="object_id"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id=None, + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + # Invalid rod_id: Empty string + with pytest.raises(ValueError, match="object_id"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_date_time() -> None: + """Test initialization of settlement rod measurement with invalid date_time.""" + # Invalid date_time: None + with pytest.raises(TypeError, match="date_time"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=None, + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_coordinate_reference_systems() -> ( + None +): + """Test initialization of settlement rod measurement with invalid coordinate reference systems.""" + # Invalid coordinate_reference_system: None + with pytest.raises(TypeError, match="coordinate_reference_systems"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=None, + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_rod_top_x() -> None: + """Test initialization of settlement rod measurement with invalid rod_top_x.""" + # Invalid rod_top_x: String value + with pytest.raises(TypeError, match="rod_top_x"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x="123340.266", + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_rod_top_y() -> None: + """Test initialization of settlement rod measurement with invalid rod_top_y.""" + # Invalid rod_top_y: None + with pytest.raises(TypeError, match="rod_top_y"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=None, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_rod_top_z() -> None: + """Test initialization of settlement rod measurement with invalid rod_top_z.""" + # Invalid rod_top_z: String value + with pytest.raises(TypeError, match="rod_top_z"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z="0.807", + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_rod_length() -> None: + """Test initialization of settlement rod measurement with invalid rod_length.""" + # Invalid point_z: String value + with pytest.raises(TypeError, match="rod_length"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length="2.0", + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + # Invalid rod_length: Negative value + with pytest.raises(ValueError, match="rod_length"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=-2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_ground_surface_z() -> None: + """Test initialization of settlement rod measurement with invalid ground_surface_z.""" + # Invalid ground_surface_z: String value + with pytest.raises(TypeError, match="ground_surface_z"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z="0.419", + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_rod_bottom_z() -> None: + """Test initialization of settlement rod measurement with invalid rod_bottom_z.""" + # Invalid rod_bottom_z: String value + with pytest.raises(TypeError, match="rod_bottom_z"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z="-1.193", + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_temperature() -> None: + """Test initialization of settlement rod measurement with invalid temperature.""" + # Invalid temperature: String value + with pytest.raises(TypeError, match="temperature"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature="12.0", + voltage=4232, + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_voltage() -> None: + """Test initialization of settlement rod measurement with invalid voltage.""" + # Invalid voltage: String value + with pytest.raises(TypeError, match="voltage"): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage="4232", + comment="No comment", + ) + + +def test_settlement_rod_measurement_init_with_invalid_comment() -> None: + """Test initialization of settlement rod measurement with invalid comment.""" + # Invalid comment: Integer value + with pytest.raises(TypeError): + SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( + 28992, 5709 + ), + rod_top_x=123340.266, + rod_top_y=487597.154, + rod_top_z=0.807, + rod_length=2.0, + rod_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, + temperature=12.0, + voltage=4232, + comment=123, + ) diff --git a/tests/measurements/test_settlement_rod_measurement_series.py b/tests/measurements/test_settlement_rod_measurement_series.py new file mode 100644 index 0000000..18c6ab9 --- /dev/null +++ b/tests/measurements/test_settlement_rod_measurement_series.py @@ -0,0 +1,266 @@ +from copy import deepcopy +from typing import List + +import pytest +from matplotlib import pyplot as plt +from pandas import show_versions + +from baec.coordinates import CoordinateReferenceSystems +from baec.measurements.measurement_device import MeasurementDevice +from baec.measurements.settlement_rod_measurement import SettlementRodMeasurement +from baec.measurements.settlement_rod_measurement_series import ( + SettlementRodMeasurementSeries, +) +from baec.project import Project + + +def test_settlement_rod_measurement_series_init_with_valid_input( + example_settlement_rod_measurements: List[SettlementRodMeasurement], +) -> None: + """Test initialization of SettlementRodMeasurementSeries with valid input.""" + + # Create series from measurements in chronological order. + series = SettlementRodMeasurementSeries( + measurements=example_settlement_rod_measurements + ) + + assert series.measurements == example_settlement_rod_measurements + + # Check that the measurements are in chronological order. + assert sorted(series.measurements, key=lambda x: x.date_time) == series.measurements + + # Create series from measurements in inverse chronological order. + series = SettlementRodMeasurementSeries( + measurements=example_settlement_rod_measurements[::-1] + ) + + assert series.measurements == example_settlement_rod_measurements + + # Check that the measurements are in chronological order. + assert sorted(series.measurements, key=lambda x: x.date_time) == series.measurements + + +def test_settlement_rod_measurement_series_init_with_invalid_measurements( + example_settlement_rod_measurements: List[SettlementRodMeasurement], +) -> None: + """Test initialization of SettlementRodMeasurementSeries with invalid measurements.""" + + # Empty list + with pytest.raises(ValueError, match="measurements"): + SettlementRodMeasurementSeries(measurements=[]) + + # Incorrect type: One item is a string. + measurements = deepcopy(example_settlement_rod_measurements) + measurements[0] = "invalid" + with pytest.raises(TypeError): + SettlementRodMeasurementSeries(measurements=measurements) + + # Different projects + measurements = deepcopy(example_settlement_rod_measurements) + measurements[0]._project = Project(id_="P-002", name="Project 2") + + with pytest.raises(ValueError, match="project"): + SettlementRodMeasurementSeries(measurements=measurements) + + # Different devices + measurements = deepcopy(example_settlement_rod_measurements) + measurements[0]._device = MeasurementDevice(id_="BR_004", qr_code="QR-004") + + with pytest.raises(ValueError, match="device"): + SettlementRodMeasurementSeries(measurements=measurements) + + # Different measured objects + measurements = deepcopy(example_settlement_rod_measurements) + measurements[0]._object_id = "ZB-20" + + with pytest.raises(ValueError, match="object"): + SettlementRodMeasurementSeries(measurements=measurements) + + # Different coordinate reference systems (horizontal) + measurements = deepcopy(example_settlement_rod_measurements) + measurements[ + 0 + ]._coordinate_reference_systems = CoordinateReferenceSystems.from_epsg(28992, 5710) + + with pytest.raises(ValueError, match="coordinate reference systems"): + SettlementRodMeasurementSeries(measurements=measurements) + + # Different coordinate reference systems (vertical) + measurements = deepcopy(example_settlement_rod_measurements) + measurements[ + 0 + ]._coordinate_reference_systems = CoordinateReferenceSystems.from_epsg(31370, 5709) + + with pytest.raises(ValueError, match="coordinate reference systems"): + SettlementRodMeasurementSeries(measurements=measurements) + + +def test_settlement_rod_measurement_series_to_dataframe_method( + example_settlement_rod_measurements: List[SettlementRodMeasurement], +) -> None: + """Test the to_dataframe method of SettlementRodMeasurementSeries.""" + series = SettlementRodMeasurementSeries( + measurements=example_settlement_rod_measurements + ) + + df = series.to_dataframe() + + # Check that the DataFrame has the correct number of rows. + assert len(df) == len(example_settlement_rod_measurements) + + # Check that the DataFrame has the correct data. + for i, measurement in enumerate(example_settlement_rod_measurements): + assert df.iloc[i]["project_id"] == measurement.project.id + assert df.iloc[i]["device_id"] == measurement.device.id + assert df.iloc[i]["object_id"] == measurement.object_id + assert ( + df.iloc[i]["coordinate_horizontal_epsg_code"] + == measurement.coordinate_reference_systems.horizontal.to_epsg() + ) + assert ( + df.iloc[i]["coordinate_vertical_epsg_code"] + == measurement.coordinate_reference_systems.vertical.to_epsg() + ) + assert df.iloc[i]["coordinate_horizontal_units"] == ( + measurement.coordinate_reference_systems.horizontal_units + ) + assert df.iloc[i]["coordinate_vertical_units"] == ( + measurement.coordinate_reference_systems.vertical_units + ) + assert df.iloc[i]["coordinate_vertical_datum"] == ( + measurement.coordinate_reference_systems.vertical_datum + ) + assert df.iloc[i]["date_time"] == measurement.date_time + assert df.iloc[i]["rod_top_x"] == measurement.rod_top_x + assert df.iloc[i]["rod_top_y"] == measurement.rod_top_y + assert df.iloc[i]["rod_top_z"] == measurement.rod_top_z + assert df.iloc[i]["rod_length"] == measurement.rod_length + assert df.iloc[i]["rod_bottom_z"] == measurement.rod_bottom_z + assert ( + df.iloc[i]["rod_bottom_z_uncorrected"] + == measurement.rod_bottom_z_uncorrected + ) + assert df.iloc[i]["ground_surface_z"] == measurement.ground_surface_z + assert df.iloc[i]["status"] == measurement.status.value + assert df.iloc[i]["temperature"] == measurement.temperature + assert df.iloc[i]["voltage"] == measurement.voltage + assert df.iloc[i]["comment"] == measurement.comment + + +def test_plot_x_time( + example_settlement_rod_measurements: List[SettlementRodMeasurement], +) -> None: + """Test plot_x_time method are generated without error.""" + + show = False + + series = SettlementRodMeasurementSeries( + measurements=example_settlement_rod_measurements + ) + + # Plot without giving axes + ax = series.plot_x_time() + if show: + plt.show() + + # Plot giving axes + _, ax = plt.subplots() + series.plot_x_time(ax) + if show: + plt.show() + + plt.close("all") + + +def test_plot_y_time( + example_settlement_rod_measurements: List[SettlementRodMeasurement], +) -> None: + """Test plot_y_time method are generated without error.""" + + show = False + + series = SettlementRodMeasurementSeries( + measurements=example_settlement_rod_measurements + ) + + # Plot without giving axes + ax = series.plot_y_time() + if show: + plt.show() + + # Plot giving axes + _, ax = plt.subplots() + series.plot_y_time(ax) + if show: + plt.show() + + plt.close("all") + + +def test_plot_z_time( + example_settlement_rod_measurements: List[SettlementRodMeasurement], +) -> None: + """Test plot_z_time method are generated without error.""" + + show = False + + series = SettlementRodMeasurementSeries( + measurements=example_settlement_rod_measurements + ) + + # Plot without giving axes + ax = series.plot_z_time() + if show: + plt.show() + + # Plot giving axes + _, ax = plt.subplots() + series.plot_z_time(ax) + if show: + plt.show() + + plt.close("all") + + +def test_plot_xyz_time( + example_settlement_rod_measurements: List[SettlementRodMeasurement], +) -> None: + """Test plot_xyz_time method are generated without error.""" + + show = False + + series = SettlementRodMeasurementSeries( + measurements=example_settlement_rod_measurements + ) + + # Plot without giving axes + series.plot_xyz_time() + if show: + plt.show() + + plt.close() + + +def test_plot_xy_plan_view( + example_settlement_rod_measurements: List[SettlementRodMeasurement], +) -> None: + """Test plot_xy_plan_view method are generated without error.""" + + show = False + + series = SettlementRodMeasurementSeries( + measurements=example_settlement_rod_measurements + ) + + # Plot without giving axes + ax = series.plot_xy_plan_view() + if show: + plt.show() + + # Plot giving axes + _, ax = plt.subplots() + series.plot_xy_plan_view(ax) + if show: + plt.show() + + plt.close("all") diff --git a/tests/test_coordinates.py b/tests/test_coordinates.py new file mode 100644 index 0000000..854d06f --- /dev/null +++ b/tests/test_coordinates.py @@ -0,0 +1,88 @@ +import pyproj +import pytest + +from baec.coordinates import CoordinateReferenceSystems + + +def test_coordinate_reference_system_init_with_valid_input() -> None: + """Test initialization of CoordinateReferenceSystems with valid input.""" + + # With projected and vertical CRS + horizontal_crs = pyproj.CRS.from_epsg(28992) + vertical_crs = pyproj.CRS.from_epsg(5710) + crs = CoordinateReferenceSystems(horizontal=horizontal_crs, vertical=vertical_crs) + assert crs.horizontal == horizontal_crs + assert crs.vertical == vertical_crs + + # With compound CRS + horizontal_crs = pyproj.CRS.from_epsg(7415) + vertical_crs = pyproj.CRS.from_epsg(7415) + crs = CoordinateReferenceSystems(horizontal=horizontal_crs, vertical=vertical_crs) + assert crs.horizontal == horizontal_crs + assert crs.vertical == vertical_crs + + +def test_coordinate_reference_system_init_with_invalid_horizontal_CRS() -> None: + """Test initialization of CoordinateReferenceSystems with invalid horizontal CRS.""" + # Invalid horizontal CRS: None + with pytest.raises(TypeError, match="horizontal"): + CoordinateReferenceSystems(horizontal=None, vertical=pyproj.CRS.from_epsg(5710)) + + # Invalid horizontal CRS: Vertical CRS + with pytest.raises(ValueError, match="horizontal"): + CoordinateReferenceSystems( + horizontal=pyproj.CRS.from_epsg(5710), vertical=pyproj.CRS.from_epsg(5710) + ) + + +def test_coordinate_reference_system_init_with_invalid_vertical_CRS() -> None: + """Test initialization of CoordinateReferenceSystems with invalid vertical CRS.""" + # Invalid vertical CRS: None + with pytest.raises(TypeError, match="vertical"): + CoordinateReferenceSystems( + horizontal=pyproj.CRS.from_epsg(28992), vertical=None + ) + + # Invalid vertical: Projected CRS + with pytest.raises(ValueError, match="vertical"): + CoordinateReferenceSystems( + horizontal=pyproj.CRS.from_epsg(28992), vertical=pyproj.CRS.from_epsg(28992) + ) + + +def test_coordinate_reference_system_from_epsg() -> None: + """Test constructor method `from_espg`.""" + # Valid input + crs = CoordinateReferenceSystems.from_epsg(horizontal=28992, vertical=5710) + assert crs.horizontal == pyproj.CRS.from_epsg(28992) + assert crs.vertical == pyproj.CRS.from_epsg(5710) + + # Invalid horizontal EPSG code + with pytest.raises(pyproj.exceptions.CRSError): + CoordinateReferenceSystems.from_epsg(horizontal=99999, vertical=5710) + + # Invalid vertical EPSG code + with pytest.raises(pyproj.exceptions.CRSError): + CoordinateReferenceSystems.from_epsg(horizontal=28992, vertical=99999) + + +def test_coordinate_reference_system__eq__method() -> None: + """Test the __eq__ method of CoordinateReferenceSystems.""" + crs_1 = CoordinateReferenceSystems.from_epsg(28992, 5709) + crs_2 = CoordinateReferenceSystems.from_epsg(28992, 5709) + crs_3 = CoordinateReferenceSystems.from_epsg(31370, 5710) + crs_4 = CoordinateReferenceSystems.from_epsg(28992, 5710) + crs_5 = CoordinateReferenceSystems.from_epsg(31370, 5709) + + assert crs_1 == crs_2 + assert crs_1 != crs_3 + assert crs_1 != crs_4 + assert crs_1 != crs_5 + assert crs_2 != crs_3 + assert crs_2 != crs_4 + assert crs_3 != crs_4 + assert crs_4 != crs_5 + + assert crs_1 == crs_1 + assert crs_1 != None + assert crs_1 != "EPSG:4326" diff --git a/tests/test_project.py b/tests/test_project.py new file mode 100644 index 0000000..9e67ec7 --- /dev/null +++ b/tests/test_project.py @@ -0,0 +1,50 @@ +import pytest + +from baec.project import Project + + +def test_project_with_valid_input() -> None: + """Test initialization of Project with valid input.""" + project = Project(id_="P001", name="name_1") + assert project.id == "P001" + assert project.name == "name_1" + + +def test_project_init_with_invalid_id() -> None: + """Test initialization of Project with invalid ID.""" + # Invalid id: None + with pytest.raises(TypeError, match="id"): + Project(id_=None, name="name_1") + + # Invalid id: Empty string + with pytest.raises(ValueError, match="id"): + Project(id_="", name="name_1") + + +def test_project_init_with_invalid_name() -> None: + """Test initialization of Project with invalid name.""" + # Invalid name: None + with pytest.raises(TypeError, match="name"): + Project(id_="P001", name=None) + + # Invalid name: Empty string + with pytest.raises(ValueError, match="name"): + Project(id_="P001", name="") + + +def test_project__eq__method() -> None: + """Test the __eq__ method of Project.""" + project_1 = Project(id_="P001", name="name_1") + project_2 = Project(id_="P001", name="name_1") + project_3 = Project(id_="P002", name="name_2") + project_4 = Project(id_="P002", name="name_1") + + assert project_1 == project_2 + assert project_1 != project_3 + assert project_1 != project_4 + assert project_2 != project_3 + assert project_3 != project_4 + + assert project_1 == project_1 + assert project_1 != None + assert project_1 != "P001"