Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add section about time-dependent SpatialData to CSM tutorial #792

Merged
merged 6 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ changes
=======
- Updated the outdated tutorial about the `LocalCoordinateSystem` [:pull:`775`]
- ``weld_seam`` is now a required field in the ``multi_pass_weld`` schema [:pull:`790`]
- Add section about time-dependent spatial data to the `CoordinateSystemManager` tutorial [:pull:`792`]

fixes
=====
Expand Down
328 changes: 327 additions & 1 deletion tutorials/transformations_02_coordinate_system_manager.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"source": [
"from copy import deepcopy\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import xarray as xr\n",
"\n",
Expand Down Expand Up @@ -1029,6 +1030,331 @@
"Alternatively, if you want to decompose a `CoordinateSystemManager` instance into all its subsystems, you can use the `unmerge` function.\n",
"It works exactly the same as the `subsystems` property with the difference that it also removes all subsystem data from the affected `CoordinateSystemManager` instance."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Time dependent spatial data\n",
"\n",
">This chapter covers an advanced topic and requires you to have a solid understanding of the previously discussed topics, especially the ones about time dependencies.\n",
"Additionally, you should also be familiar with the `SpatialData` class.\n",
"\n",
"\n",
"\n",
"Time dependent spatial data is simply spatial data that gets another dimension that is associated with time.\n",
"A possible way to utilize this would be for example to gather the different geometry states between multiple welding passes.\n",
"\n",
"However, in the context of the `CoordinateSystemManager`, it is used to construct a complex 3d geometry from multiple, time-dependent subsets.\n",
"If you wonder what actual use-case this might have, think of a laser scanner that scans a geometry from multiple positions and angles.\n",
"All the data is usually captured in the scanners own coordinate system.\n",
"This means that all scans lie initially on top of each other.\n",
"If we know the time-dependent coordinates and orientation of the scanner as well as the time at which each scan was taken, we can reconstruct the whole scanned object, given we know all the transformations to the scanned objects coordinate system.\n",
"The `CoordinateSystemManager` does this automatically if you pass time-dependent `SpatialData` to it.\n",
"\n",
"To demonstrate how this is done, we will look at a small example.\n",
"First we create the data of a 2d-scanner:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"num_scan_n = 20\n",
"\n",
"scan_width_in_mm = 4\n",
"\n",
"\n",
"dx_scan = scan_width_in_mm / (num_scan_n - 1)\n",
"scan_profile = np.array(\n",
" [\n",
" [\n",
" 0,\n",
" i * dx_scan - scan_width_in_mm / 2,\n",
" np.sin(i * np.pi / (num_scan_n - 1)) - 2,\n",
" ]\n",
" for i in range(num_scan_n)\n",
" ]\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is just a simple, single sine-wave:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.plot(scan_profile[:, 1], scan_profile[:, 2])\n",
"plt.xlabel(\"y in mm\")\n",
"plt.ylabel(\"z in mm\");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now lets say we scanned 40 profiles over a period of 20 seconds.\n",
"For simplicity, we reuse the same profile for every scan."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"num_scan_p = 40\n",
"duration_scan_in_sec = 20\n",
"dt_scan = duration_scan_in_sec / (num_scan_p - 1)\n",
"time_p = Q_([i * dt_scan for i in range(num_scan_p)], \"s\")\n",
"\n",
"scan_time = Q_([i * dt_scan for i in range(num_scan_p)], \"s\")\n",
"scan_data = Q_([scan_profile for _ in range(num_scan_p)], \"mm\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that we now have a 3 dimensional `scan_data` array with the outer dimension being time.\n",
"The next one represents the individual points of a scan and the last one the 3 spatial coordinates:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"scan_data.shape"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The content of the next cell is purely to get a better visualization and can be omitted.\n",
"We create a list of triplets.\n",
"Each of those tells the SpatialData the indices of the data points that form a triangle.\n",
"This enables the `CoordinateSystemManager` to plot the spatial data as a closed surface instead of a point cloud."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"triangles = []\n",
"for j in range(num_scan_p - 1):\n",
" for i in range(num_scan_n - 1):\n",
" offset = j * num_scan_n + i\n",
" triangles.append([offset, offset + num_scan_n, offset + 1])\n",
" triangles.append([offset + 1, offset + num_scan_n, offset + num_scan_n + 1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"With all the data gathered, we can create our `SpatialData` class:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"scans = SpatialData(coordinates=scan_data, triangles=triangles, time=time_p)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we need a `CoordinateSystemManager` where we can attach the data."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"csm_scan = CoordinateSystemManager(\"base\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To get things a bit more interesting, let's say our specimen is placed on a table that rotates 180 degrees during the scan process.\n",
"Note that we will use some unrealistically small distances so that the final plot is more compact for demonstration purposes."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"table_rotation_degrees = 180\n",
"num_timesteps_csm = 101\n",
"\n",
"dt_csm = duration_scan_in_sec / (num_timesteps_csm - 1)\n",
"time_csm = Q_([i * dt_csm for i in range(num_timesteps_csm)], \"s\")\n",
"\n",
"\n",
"deg_per_step = table_rotation_degrees / (num_timesteps_csm - 1)\n",
"angles_table = [i * deg_per_step for i in range(num_timesteps_csm)]\n",
"\n",
"\n",
"csm_scan.create_cs_from_euler(\n",
" \"table\",\n",
" \"base\",\n",
" sequence=\"z\",\n",
" angles=angles_table,\n",
" degrees=True,\n",
" coordinates=Q_([-2, -2, -2], \"mm\"),\n",
" time=time_csm,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The specimen is placed at a certain offset to the tables center of rotation:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"csm_scan.create_cs(\"specimen\", \"table\", coordinates=Q_([-1, 3, 2], \"mm\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The scanner itself is mounted at a movable robot arm that has its own coordinate system which we call \"tcp\" (tool center point).\n",
"During the scanning process, the robot arm performs a linear translation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"tcp_mm_per_step = -5 / (num_timesteps_csm - 1)\n",
"coordinates_tcp = Q_(\n",
" [[3, i * tcp_mm_per_step + 12, 10] for i in range(num_timesteps_csm)], \"mm\"\n",
")\n",
"\n",
"csm_scan.create_cs(\"tcp\", \"base\", coordinates=coordinates_tcp, time=time_csm)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The scanners coordinate system has a fixed offset to the robot arms coordinate system."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"csm_scan.create_cs(\"scanner\", \"tcp\", coordinates=Q_([0, 0, 2], \"mm\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Our complete `CoordinateSystemManager` has the following structure:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"csm_scan"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Remember that the coloured arrows represent time-dependent relations.\n",
"\n",
"Now let's recapitulate what we are going to do.\n",
"We got a set of scan profiles of a specimen measured at different points in time in the scanner coordinate system.\n",
"The object we scanned is static in the specimen coordinate system.\n",
"So we tell the `CoordinateSystemManager` that the time-dependent data we want to add was measured in the scanner coordinate system but should be transformed into the specimen coordinate system.\n",
"This is done as follows:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"csm_scan.assign_data(\n",
" scans, data_name=\"scans\", reference_system=\"scanner\", target_system=\"specimen\"\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For each time step of `SpatialData`, the `CoordinateSystemManager` now calculates the correct, time-dependent transformation from the scanner to the specimen coordinate system.\n",
"Those transformations are then applied to the data before it is stored.\n",
"Let's plot the `CoordinateSystemManager` and check if everything worked as expected."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"csm_scan.plot(backend=\"k3d\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To understand the plot, it is best to play the animation and see how everything moves in relation to each other.\n",
"\n",
"Due to the rotating table, we get a circular shape.\n",
"Because the scanner was also performing a translation towards the center of rotation, we do also get a varying radius.\n",
"Since we used the same profile for each time step, the resulting geometry has the same cross-section everywhere and it looks like the scanner is just following the peak of the geometry.\n",
"\n",
"Of cause, this is a very simplistic example, but it demonstrates how easy it is to reconstruct scan data with the `CoordinateSystemManager`, regardless of how complex the relative motions of the different involved coordinate systems are.\n",
"Reconstructing real scan data wouldn't be any different."
]
}
],
"metadata": {
Expand All @@ -1047,7 +1373,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.4"
"version": "3.10.5"
}
},
"nbformat": 4,
Expand Down
17 changes: 14 additions & 3 deletions weldx/transformations/cs_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,16 @@ def assign_data(
):
"""Assign spatial data to a coordinate system.

If the assigned data is time-dependent and a target system is specified, the
data will be transformed considering all time-dependencies on the transformation
path. This is especially useful to reconstruct specimen geometries from scan
data. If the raw scan data contains the timestamps of the scans and the
movement of the scanner is described by a corresponding time-dependent
coordinate system, the `CoordinateSystemManager` is able to calculate the
specimen geometry on its own. Therefore, you need to provide the scanner
coordinate system name as the ``reference_system`` parameter and the specimen
coordinate system name as the ``target_system`` parameter.

Parameters
----------
data
Expand All @@ -575,9 +585,10 @@ def assign_data(
Name of the coordinate system the data values are defined in.
target_system:
Name of the target system the data will be transformed and assigned to.
This is useful when adding time-dependent data. The provided name must match
an existing system. If `None` is passed (the default), data will not be
transformed and assigned to the 'reference_system'.
This is useful when adding time-dependent data (see function description).
The provided name must match an existing system. If `None` is passed
(the default), data will not be transformed and assigned to the
'reference_system'.

"""
if not isinstance(data_name, str):
Expand Down