diff --git a/.github/workflows/build_pkg.yml b/.github/workflows/build_pkg.yml index e6b690813..5bcce07fc 100644 --- a/.github/workflows/build_pkg.yml +++ b/.github/workflows/build_pkg.yml @@ -41,7 +41,7 @@ jobs: fetch-depth: 0 # Fetch all history for all tags and branches - name: Setup Conda Environment - uses: mamba-org/setup-micromamba@v1.9.0 + uses: mamba-org/setup-micromamba@v2.0.1 with: environment-file: ./devtools/conda.recipe/build_env.yml environment-name: build_env diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 156c4e416..3f88b2b38 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 # Fetch all history for all tags and branches - name: Setup Conda Environment - uses: mamba-org/setup-micromamba@v1.9.0 + uses: mamba-org/setup-micromamba@v2.0.1 with: environment-file: ./doc/rtd_environment.yml environment-name: rtd diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 3588e9b4e..70704748f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -33,13 +33,13 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - py: ['3.9', '3.10', '3.11'] + py: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 with: fetch-depth: '0' # Fetch all history for all tags and branches - - uses: CagtayFabry/pydeps2env@v1.2.0 + - uses: marscher/pydeps2env@v999 with: files: 'pyproject.toml' channels: 'conda-forge defaults' @@ -47,14 +47,16 @@ jobs: build_system: 'include' - name: Setup Conda Environment - uses: mamba-org/setup-micromamba@v1.9.0 + uses: mamba-org/setup-micromamba@v2.0.1 with: + micromamba-version: "2.0.2-0" environment-file: ./environment.yml environment-name: weldx init-shell: >- bash powershell - cache-environment: true + # persist on the same day. + cache-environment-key: environment-${{ steps.date.outputs.date }} create-args: >- python=${{ matrix.py }} wheel @@ -133,13 +135,13 @@ jobs: fail-fast: false matrix: os: [windows-latest] - py: ['3.9'] + py: ['3.10'] steps: - uses: actions/checkout@v4 with: fetch-depth: '0' # Fetch all history for all tags and branches - - uses: CagtayFabry/pydeps2env@v1.2.0 + - uses: marscher/pydeps2env@v999 with: files: 'pyproject.toml' channels: 'conda-forge defaults' @@ -147,14 +149,15 @@ jobs: build_system: 'include' - name: Setup Conda Environment - uses: mamba-org/setup-micromamba@v1.9.0 + uses: mamba-org/setup-micromamba@v2.0.1 with: environment-file: ./environment.yml environment-name: weldx init-shell: >- bash powershell - cache-environment: true + # persist on the same day. + cache-environment-key: environment-${{ steps.date.outputs.date }} create-args: >- python=${{ matrix.py }} wheel diff --git a/.github/workflows/pytest_asdf.yml b/.github/workflows/pytest_asdf.yml index e16d32052..fef72fe85 100644 --- a/.github/workflows/pytest_asdf.yml +++ b/.github/workflows/pytest_asdf.yml @@ -33,7 +33,7 @@ jobs: setup_requires: 'include' - name: Setup Conda Environment - uses: mamba-org/setup-micromamba@v1.9.0 + uses: mamba-org/setup-micromamba@v2.0.1 with: environment-file: ./environment.yml environment-name: weldx diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 85288e8d4..d66d625f9 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -49,13 +49,13 @@ jobs: key: ${{ runner.os }}-${{ hashFiles('./environment.yml') }} - name: Setup Conda Environment - uses: mamba-org/setup-micromamba@v1.9.0 + uses: mamba-org/setup-micromamba@v2.0.1 with: environment-file: ./environment.yml environment-name: weldx cache-environment: true create-args: >- - python=3.9 + python=3.10 mypy - name: activate env run: micromamba activate weldx diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d704d57e1..76d7d02fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ exclude: '.*.weldx$|.*.wx$|.*.asdf$' repos: # ----- general formatting ----- - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] @@ -20,7 +20,7 @@ repos: - id: check-yaml exclude: devtools/conda.recipe/meta.yaml # doesn't play nice with jinja - repo: https://github.com/executablebooks/mdformat - rev: 0.7.17 + rev: 0.7.18 hooks: - id: mdformat additional_dependencies: @@ -29,7 +29,7 @@ repos: - mdformat-config # ----- Python formatting ----- - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.3 + rev: v0.7.2 hooks: # Run ruff linter. - id: ruff @@ -39,11 +39,11 @@ repos: # Run ruff formatter. - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.1 + rev: v2.5.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.19 + rev: v0.22 hooks: - id: validate-pyproject # ----- Jupyter Notebooks ----- diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a86ea0aa..0f59ffc71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## 0.6.9 (unreleased) +### Changes + +- added support for homogeneous transformation matrices \[{pull}`949`\]: + - added `create_cs_from_homogeneous_transformation` to `CoordinateSystemManager` + - added `from_homogeneous_transformation` to `LocalCoordinateSystem` + - added `as_homogeneous_matrix` to `LocalCoordinateSystem` + ### Fixes - rename (fix typo) argument to `lcs_child_in_parent` in `CoordinateSystemManager.add_cs` \[{pull}`936`\]. @@ -12,6 +19,7 @@ - pin `weldx-widgets>=0.2.3` for viz \[{pull}`939`\]. - pin `pint>=0.21` \[{pull}`941`\]. +- pin `python<3.13` \[{pull}`896`\]. ## 0.6.8 (07.06.2024) diff --git a/doc/json_mime_render_plugin/pyproject.toml b/doc/json_mime_render_plugin/pyproject.toml index 788035bed..4900aeca5 100644 --- a/doc/json_mime_render_plugin/pyproject.toml +++ b/doc/json_mime_render_plugin/pyproject.toml @@ -3,10 +3,10 @@ name = "myst-nb-json-renderer" version = "1" classifiers = [ "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] entry-points."myst_nb.mime_renderers".json_mime = "myst_nb_json_render_plugin:MimeRenderPlugin" diff --git a/pyproject.toml b/pyproject.toml index 4fd4335f2..1e61ed9b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ authors = [ { name = "Volker Hirthammer", email = "volker.hirthammer@bam.de" }, { name = "Martin K. Scherer", email = "martin.scherer@bam.de" }, ] -requires-python = ">=3.9,<3.12" +requires-python = ">=3.9,<3.13" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Education", @@ -34,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering :: Physics", # TODO: add more topics here! ] dynamic = [ diff --git a/weldx/tests/transformations/test_cs_manager.py b/weldx/tests/transformations/test_cs_manager.py index 768e16ada..48aeb2eb7 100644 --- a/weldx/tests/transformations/test_cs_manager.py +++ b/weldx/tests/transformations/test_cs_manager.py @@ -2749,6 +2749,10 @@ def test_coordinate_system_manager_create_coordinate_system(): orientations = np.matmul(rot_mat_x, rot_mat_y) coords = Q_([[1, 0, 0], [-1, 0, 2], [3, 5, 7], [-4, -5, -6]], "mm") + transformation_matrix = np.resize(np.identity(4), (4, 4, 4)) + transformation_matrix[:, :3, :3] = orientations + transformation_matrix[:, :3, 3] = coords.m + csm = tf.CoordinateSystemManager("root") lcs_default = tf.LocalCoordinateSystem() @@ -2790,6 +2794,18 @@ def test_coordinate_system_manager_create_coordinate_system(): time=time, ) + # from homogeneous transformation --------------------- + csm.create_cs_from_homogeneous_transformation( + "lcs_homogeneous_default", "root", transformation_matrix, coords.u, time + ) + check_coordinate_system( + csm.get_cs("lcs_homogeneous_default"), + orientations, + coords, + True, + time=time, + ) + def test_coordinate_system_manager_transform_data(): """Test the coordinate system managers transform_data function.""" diff --git a/weldx/transformations/cs_manager.py b/weldx/transformations/cs_manager.py index 1cb4ff002..b8f305376 100644 --- a/weldx/transformations/cs_manager.py +++ b/weldx/transformations/cs_manager.py @@ -17,10 +17,11 @@ from weldx.exceptions import WeldxDeprecationWarning, WeldxException from weldx.geometry import SpatialData from weldx.time import Time, types_time_like, types_timestamp_like +from weldx.types import UnitLike from weldx.util import check_matplotlib_available, dataclass_nested_eq from .local_cs import LocalCoordinateSystem -from .types import types_coordinates, types_orientation +from .types import types_coordinates, types_homogeneous, types_orientation # only import heavy-weight packages on type checking if TYPE_CHECKING: # pragma: no cover @@ -830,6 +831,51 @@ def create_cs_from_axis_vectors( coordinate_system_name, reference_system_name, lcs, lcs_child_in_parent ) + def create_cs_from_homogeneous_transformation( + self, + coordinate_system_name: str, + reference_system_name: str, + transformation_matrix: types_homogeneous, + translation_unit: UnitLike, + time: types_time_like = None, + time_ref: types_timestamp_like = None, + lcs_child_in_parent: bool = True, + ): + """Create a coordinate system from a homogeneous transformation matrix and add + it to the coordinate system manager. + + This function uses the `LocalCoordinateSystem.from_homogeneous_transformation` + method of the `LocalCoordinateSystem` class. + + Parameters + ---------- + coordinate_system_name : + Name of the new coordinate system. + reference_system_name : + Name of the parent system. This must have been already added. + transformation_matrix : + Describes the homogeneous transformation matrix that includes the rotation + and the translation (coordinates). + translation_unit : + Unit describing the value of the translation. Necessary, because the + homogeneous transformation matrix is unitless. + time : + Time data for time dependent coordinate systems. + time_ref : + Reference time for time dependent coordinate systems + lcs_child_in_parent : + If set to `True`, the passed `LocalCoordinateSystem` instance describes + the new system orientation towards is parent. If `False`, it describes + how the parent system is positioned in its new child system. + + """ + lcs = LocalCoordinateSystem.from_homogeneous_transformation( + transformation_matrix, translation_unit, time, time_ref + ) + self.add_cs( + coordinate_system_name, reference_system_name, lcs, lcs_child_in_parent + ) + def delete_cs(self, coordinate_system_name: str, delete_children: bool = False): """Delete a coordinate system from the coordinate system manager. diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index ac976d776..ae4dfb9e6 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -18,8 +18,13 @@ from weldx.core import TimeSeries from weldx.exceptions import WeldxException from weldx.time import Time, TimeDependent, types_time_like, types_timestamp_like -from weldx.transformations.types import types_coordinates, types_orientation +from weldx.transformations.types import ( + types_coordinates, + types_homogeneous, + types_orientation, +) from weldx.transformations.util import normalize +from weldx.types import UnitLike __all__ = ("LocalCoordinateSystem",) @@ -540,6 +545,45 @@ def from_axis_vectors( t_axes = (1, 0) if mat.ndim == 2 else (1, 2, 0) return cls(mat.transpose(t_axes), coordinates, time, time_ref) + @classmethod + def from_homogeneous_transformation( + cls, + transformation_matrix: types_homogeneous, + translation_unit: UnitLike, + time: types_time_like = None, + time_ref: types_timestamp_like = None, + ) -> LocalCoordinateSystem: + """Construct a local coordinate system from a homogeneous transformation matrix. + + Parameters + ---------- + transformation_matrix : + Describes the homogeneous transformation matrix that includes the rotation + and the translation (coordinates). + translation_unit : + Unit describing the value of the translation. Necessary, because the + homogeneous transformation matrix is unitless. + time : + Time data for time dependent coordinate systems (Default value = None) + time_ref : + Optional reference timestamp if ``time`` is a time delta. + + Returns + ------- + LocalCoordinateSystem + Local coordinate system + + """ + if isinstance(transformation_matrix, xr.DataArray): + transformation_matrix = np.array(transformation_matrix.data) + if transformation_matrix.ndim == 3: + orientation = transformation_matrix[:, :3, :3] + coordinates = Q_(transformation_matrix[:, :3, 3], translation_unit) + else: + orientation = transformation_matrix[:3, :3] + coordinates = Q_(transformation_matrix[:3, 3], translation_unit) + return cls(orientation, coordinates=coordinates, time=time, time_ref=time_ref) + @property def orientation(self) -> xr.DataArray: """Get the coordinate systems orientation matrix. @@ -690,6 +734,44 @@ def as_rotation(self) -> Rot: # pragma: no cover """ return Rot.from_matrix(self.orientation.values) + def as_homogeneous_matrix(self, translation_unit: UnitLike) -> np.ndarray: + """Get a homogeneous transformation matrix from the coordinate system + orientation. + + Parameters + ---------- + translation_unit : UnitLike + Unit the translation part of the homogeneous transformation matrix + should represent. + + Returns + ------- + numpy.ndarray + Numpy array representing the homogeneous transformation matrix. + + """ + + if self.is_time_dependent: + time_dim = self.time.shape[0] + else: + time_dim = 1 + + rotation = np.resize(self.orientation.data, (time_dim, 3, 3)) + coordinates = self.coordinates + if not isinstance(coordinates, TimeSeries): + translation = np.resize( + coordinates.data.to(translation_unit).m, (time_dim, 3) + ) + homogeneous_matrix = np.resize(np.identity(4), (time_dim, 4, 4)) + homogeneous_matrix[:, :3, :3] = rotation + homogeneous_matrix[:, :3, 3] = translation + + return np.squeeze(homogeneous_matrix) + else: + raise NotImplementedError( + "Cannot convert LCS with `TimeSeries` coordinates to homogeneous matrix" + ) + def _interp_time_orientation(self, time: Time) -> xr.DataArray: """Interpolate the orientation in time.""" if "time" not in self.orientation.dims: # don't interpolate static diff --git a/weldx/transformations/types.py b/weldx/transformations/types.py index 2e724913d..945fcd069 100644 --- a/weldx/transformations/types.py +++ b/weldx/transformations/types.py @@ -2,6 +2,7 @@ from typing import Union +import numpy as np import numpy.typing as npt import pint import xarray as xr @@ -9,9 +10,11 @@ types_coordinates = Union[xr.DataArray, npt.ArrayLike, pint.Quantity] types_orientation = Union[xr.DataArray, npt.ArrayLike, Rotation] +types_homogeneous = Union[xr.DataArray, np.ndarray] __all__ = [ "types_coordinates", "types_orientation", + "types_homogeneous", ]