2021 IIW Annual Assembly C-XII

This notebook is an extended interactive version of the weldx features and example dataset presented at the 2021 IIW Annual Assembly C-XII meeting.

The code of this notebook can be found here: https://github.com/BAMWelDX/IIW2021_AA_CXII

You can launch this notebook as an interactive binder session in your browser following this link:
https://mybinder.org/v2/gh/BAMWelDX/IIW2021_AA_CXII/main?urlpath=lab/tree/iiw2021_CXII_fabry_cagtay_01.ipynb

The weldx documentation and code is available online:
https://weldx.readthedocs.io/en/latest/
https://github.com/BAMWelDX/weldx

The weldx documentation and GitHub links for this specific code version v0.6.9 can be found here:
https://weldx.readthedocs.io/en/v0.6.9/index.html
https://github.com/BAMWelDX/weldx/tree/v0.6.9

Process Video

To give an ovierview of this welding example, here is a video recording of the welding experiment conducted at BAM.

We can see the pre- and post-welding scan of the workpiece geometry as well as the position of the temperature measurements.

Imports

We start with some general python package imports used throughout this notebook.

import pprint

import matplotlib.pyplot as plt

from weldx import Q_, WeldxFile
from weldx.asdf.util import get_schema_path, view_tree

pp = pprint.PrettyPrinter(indent=2)
pprint = pp.pprint

Some helper functions for this notebook are included in the helpers.py file.

# default plotting styles
plt.rcParams.update({"axes.grid": True, "figure.figsize": (10, 6)})
from helpers import (
    ax_setup,
    build_base_csm,
    create_geometry,
    cs_colors,
    plot_gmaw,
    plot_measurements,
    plot_signal,
    welding_wire_geo_data,
)

1. Opening the file

We define the weldx filename that contains the data used for this notebook.

filename = "./single_pass_weld.weldx"

To get an overview of the file contents we can use the view_tree function of the weldx library to create an interactive tree view.

Try searching for a specific term like wire_feedrate using the Filter… box in the upper right.

view_tree(filename)
<IPython.core.display.JSON object>

To open and access the example dataset we will use the WeldxFile class.

wx_file = WeldxFile(filename, "r", sync=False)

You can find more infos about handling weldx files in the tutorial: https://weldx.readthedocs.io/en/latest/tutorials/weldxfile.html

The file is expected to validate against the schema single_pass_weld-0.1.0.yaml.
The schema ensures that all elements of the weldx file pass the requirements defined in the schema, including:

  • all requirement entries are present in the file:

    • workpiece

    • TCP

    • welding_current

    • welding_voltage

    • measurement chains

    • equipment

  • all entries and objects stored in the file have the correct type

  • all additional restrictions defined in single_pass_weld-0.1.0.yaml are met

The details describing the schema requirements can be found here: https://weldx.readthedocs.io/en/v0.5.0/generated/datamodels/single_pass_weld-0.1.0.html

file_schema = get_schema_path("single_pass_weld-0.1.0")

We open the weldx file and run a validation agains the single_pass_weld-0.1.0.yaml schema.

with WeldxFile(
    filename,
    "r",
    custom_schema=file_schema,
    sync=False,
    asdffile_kwargs={"copy_arrays": True},
) as file:
    wx_file = file
/home/runner/micromamba/envs/weldx/lib/python3.11/site-packages/asdf/_asdf.py:189: AsdfWarning: copy_arrays is deprecated; use memmap instead. Note that memmap will default to False in asdf 4.0.
  warnings.warn(

We can now access and explore the contents of the file interactively in our notebook.

2. General metadata

First let’s look at some general simple metadata stored in the WelDX-file.

The (optional) reference_timestamp field is used to indicate the start time of the experiment (the moment of arc ignition). All time data that is not given as absolute time are interpreted as relative to the given reference time.

wx_file.get("reference_timestamp")
Timestamp('2021-03-17 11:06:42.334400')

We can deduce the total runtime of the experiment from the TCP movement of the welding.

wx_file["TCP"].time
Time:
DatetimeIndex(['2021-03-17 11:06:42.334400', '2021-03-17 11:07:23.667733333'], dtype='datetime64[ns]', freq=None)
reference time: 2021-03-17 11:06:42.334400

The WelDX standard introduces the wx_user field to store user specific content.

wx_file.get("wx_user")
{'WID': 417, 'operator': 'C. Schippereit', 'project': 'WelDX presentation'}

We define a time index from start to end of the experiment.

t = wx_file["TCP"].time.as_timedelta_index()

3. Workpiece definition

The file schema mandates that the user provides workpiece information with the following properties:

  • base_metal referenced by a common name and the associated standard

  • the geometry consisting of a groove description following ISO 9692-1 and the seam length

Here is how this information is stored the workpiece entry of the example dataset weldx file:

workpiece:
  base_metal: {common_name: S355J2+N, standard: 'DIN EN 10225-2:2011'}
  geometry:
    groove_shape: !weldx!groove/iso_9692_1_2013_12/VGroove-0.1.0
      t: !weldx!units/quantity-0.1.0 {value: 8, units: !weldx!units/units-0.1.0 millimeter}
      alpha: !weldx!units/quantity-0.1.0 {value: 45, units: !weldx!units/units-0.1.0 degree}
      b: !weldx!units/quantity-0.1.0 {value: 1, units: !weldx!units/units-0.1.0 millimeter}
      c: !weldx!units/quantity-0.1.0 {value: 1, units: !weldx!units/units-0.1.0 millimeter}
      code_number: ['1.3', '1.5']
    seam_length: !weldx!units/quantity-0.1.0 {value: 350, units: !weldx!units/units-0.1.0 millimeter}

workpiece material

Since we know exactly where to find the information in the file, we can access the metadata directly for all files that validate against the file schema.

wx_file["workpiece"]["base_metal"]["common_name"]
'S355J2+N'
wx_file["workpiece"]["base_metal"]["standard"]
'DIN EN 10225-2:2011'

seam length

The total seam length of the workpiece is also stored.
As throughout most of the functionality of the weldx API, physical units must be used where appropriate to avoid ambiguity.

seam_length = wx_file["workpiece"]["geometry"]["seam_length"]
print(seam_length)
350 mm

welding groove

The groove shape will be loaded into a specific weldx type:

groove = wx_file["workpiece"]["geometry"]["groove_shape"]
str(groove)
"VGroove(t=<Quantity(8, 'millimeter')>, alpha=<Quantity(45, 'degree')>, c=<Quantity(1, 'millimeter')>, b=<Quantity(1, 'millimeter')>, code_number=['1.3', '1.5'])"

The weldx API includes convinient functions to create and visualize different welding groove shapes.
Many examples and details are available in this tutorial: https://weldx.readthedocs.io/en/v0.5.0/tutorials/groove_types_01.html

To get a picture of the groove shape we can simply call the plot function.

groove.plot()
fig = plt.gcf()
fig.set_size_inches(7, 7);
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/micromamba/envs/weldx/lib/python3.11/site-packages/pint_xarray/conversion.py:323, in convert_units(obj, units)
    322 try:
--> 323     new_obj = call_on_dataset(
    324         convert_units_dataset, obj, name=temporary_name, units=units
    325     )
    326 except ValueError as e:

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/pint_xarray/compat.py:13, in call_on_dataset(func, obj, name, *args, **kwargs)
     11     ds = obj
---> 13 result = func(ds, *args, **kwargs)
     15 if isinstance(obj, xr.DataArray) and isinstance(result, xr.Dataset):

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/pint_xarray/conversion.py:307, in convert_units_dataset(obj, units)
    306 if failed:
--> 307     raise ValueError(failed)
    309 reordered = {name: converted[name] for name in obj.variables.keys()}

ValueError: {('s',): ValueError('cannot convert non-quantified index')}

The above exception was the direct cause of the following exception:

ValueError                                Traceback (most recent call last)
Cell In[16], line 1
----> 1 groove.plot()
      2 fig = plt.gcf()
      3 fig.set_size_inches(7, 7);

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/decorator.py:235, in decorate.<locals>.fun(*args, **kw)
    233 if not kwsyntax:
    234     args, kw = fix(args, kw, sig)
--> 235 return caller(func, *(extras + args), **kw)

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/util/util.py:439, in check_matplotlib_available(func, *args, **kwargs)
    436     warnings.warn("Matplotlib is unavailable (module set to None).", stacklevel=2)
    437     return
--> 439 return func(*args, **kwargs)

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/welding/groove/iso_9692_1.py:147, in IsoBaseGroove.plot(self, title, axis_label, raster_width, show_params, axis, grid, line_style, ax, show_area)
    145 if raster_width is None:
    146     raster_width = Q_(0.5, _DEFAULT_LEN_UNIT)
--> 147 profile = self.to_profile()
    148 if title is None:
    149     title = _groove_type_to_name[self.__class__]

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/welding/groove/iso_9692_1.py:366, in VGroove.to_profile(self, width_default)
    363 y_value = np.append(y_value, t)
    364 segment_list.append("line")
--> 366 return self._translate_reflect(b, segment_list, x_value, y_value)

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/welding/groove/iso_9692_1.py:225, in IsoBaseGroove._translate_reflect(b, segment_list, x_value, y_value)
    223 @staticmethod
    224 def _translate_reflect(b, segment_list, x_value, y_value):
--> 225     shape = _helperfunction(segment_list, (x_value, y_value))
    226     shape = shape.translate(Q_(np.append(-b / 2, 0), _DEFAULT_LEN_UNIT))
    227     # y-axis as mirror axis

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/welding/groove/iso_9692_1.py:1650, in _helperfunction(segment, array)
   1647         segment_list.append(seg)
   1648         counter += 2
-> 1650 return geo.Shape(segment_list)

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/geometry.py:909, in Shape.__init__(self, segments)
    896 """Construct a shape.
    897 
    898 Parameters
   (...)
    906 
    907 """
    908 segments = _to_list(segments)
--> 909 self._check_segments_connected(segments)
    910 self._segments = segments

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/geometry.py:935, in Shape._check_segments_connected(segments)
    923 """Check if all segments are connected to each other.
    924 
    925 The start point of a segment must be identical to the end point of
   (...)
    932 
    933 """
    934 for i in range(len(segments) - 1):
--> 935     if not _vector_is_close(segments[i].point_end, segments[i + 1].point_start):
    936         raise ValueError("Segments are not connected.")

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/pint/registry_helpers.py:293, in wraps.<locals>.decorator.<locals>.wrapper(*values, **kw)
    287 # In principle, the values are used as is
    288 # When then extract the magnitudes when needed.
    289 new_values, new_kw, values_by_name = converter(
    290     ureg, sig, values, kw, strict
    291 )
--> 293 result = func(*new_values, **new_kw)
    295 if is_ret_container:
    296     out_units = (
    297         _replace_units(r, values_by_name) if is_ref else r
    298         for (r, is_ref) in ret
    299     )

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/geometry.py:271, in DynamicShapeSegment.point_end(self)
    267 @property
    268 @UREG.wraps(_DEFAULT_LEN_UNIT, (None,), strict=True)
    269 def point_end(self) -> pint.Quantity:
    270     """Get the end point of the segment."""
--> 271     return self.get_points(self._max_coord)[0, :2]

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/geometry.py:246, in DynamicBaseSegment.get_points(self, positions)
    232 def get_points(self, positions: float) -> pint.Quantity:
    233     """Get an array of the points at the specified relative positions.
    234 
    235     Parameters
   (...)
    244 
    245     """
--> 246     p = self._series.evaluate(**{self._series.position_dim_name: positions})
    247     return p.data_array.transpose(..., "c").data

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/core/generic_series.py:497, in GenericSeries.evaluate(self, **kwargs)
    495 if self.is_expression:
    496     return self._evaluate_expr(coords)
--> 497 return self._evaluate_array(coords)

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/core/generic_series.py:542, in GenericSeries._evaluate_array(self, coords)
    539     if k not in self.data_array.dims:
    540         raise KeyError(f"'{k}' is not a valid dimension.")
    541 return self.__class__(
--> 542     ut.xr_interp_like(self._obj, da2=eval_args, method=self._interpolation)
    543 )

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/weldx/util/xarray.py:356, in xr_interp_like(da1, da2, interp_coords, broadcast_missing, fillna, method, assume_sorted)
    349 # convert base array units to indexer units
    350 # (needed for the indexing later and it will happen during interpolation anyway)
    351 base_units = {
    352     c: da_temp[c].attrs.get(UNITS_KEY)
    353     for c in da1.coords.keys() & da_temp.coords.keys()
    354     if UNITS_KEY in da1[c].attrs
    355 }
--> 356 da1 = da1.pint.to(**base_units)
    358 # make sure edge coordinate values of da1 are in new coordinate axis of da_temp
    359 da_temp = _add_coord_edges(da1=da1, da2=da_temp, assume_sorted=assume_sorted)

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/pint_xarray/accessors.py:592, in PintDataArrayAccessor.to(self, units, **unit_kwargs)
    585     raise ValueError(
    586         "units must be either a string, a pint.Unit object or a dict-like,"
    587         f" but got {units!r}"
    588     )
    590 units = either_dict_or_kwargs(units, unit_kwargs, "to")
--> 592 return conversion.convert_units(self.da, units)

File ~/micromamba/envs/weldx/lib/python3.11/site-packages/pint_xarray/conversion.py:331, in convert_units(obj, units)
    328     if temporary_name in failed:
    329         failed[obj.name] = failed.pop(temporary_name)
--> 331     raise ValueError(format_error_message(failed, "convert")) from e
    333 return new_obj

ValueError: Cannot convert variables:
    incompatible units for variable ('s',): cannot convert non-quantified index

3D Geometry

With all the metadata of the workpiece available, it is easy to visualize a simple 3D model of the specimen.

geometry = create_geometry(groove, seam_length, Q_(10, "mm"))
geometry.plot(profile_raster_width=Q_(4, "mm"), trace_raster_width=Q_(60, "mm"))
ax_setup(plt.gca())

4. Welding TCP movement description

The path of the welding wire along the weld seam is given by the TCP property.

The weld path is a linear movement between two points at a constant weld speed. The TCP reference frame is the workpiece base coordinate system, starting at the beginning of the weld seam. The x-axis coordinates will indicate the start- and end-point of the welding process along the workpiece length. The y- and z-coordinates determine the position of the TCP in relation to the cross-sectional groove plane.

The information is stored in a LocalCoordinateSystem instance with two points and the start and end time relative to the reference_timestamp.

The YAML section of the weldx file describing the TCP movement looks like this:

TCP: !weldx!core/transformations/local_coordinate_system-0.1.0
  reference_time: !weldx!time/timestamp-0.1.0 2021-03-17T11:06:42.334400
  time: !weldx!time/timedeltaindex-0.1.0
    values: !core/ndarray-1.0.0
      data: [0, 41333333333]
      datatype: int64
      shape: [2]
    start: !weldx!time/timedelta-0.1.0 P0DT0H0M0S
    end: !weldx!time/timedelta-0.1.0 P0DT0H0M41.333333333S
    min: !weldx!time/timedelta-0.1.0 P0DT0H0M0S
    max: !weldx!time/timedelta-0.1.0 P0DT0H0M41.333333333S
  coordinates: !weldx!core/variable-0.1.0
    name: coordinates
    dimensions: [time, c]
    dtype: <f8
    data: !core/ndarray-1.0.0
      data:
      - [20.0, 0.0, 3.0]
      - [330.0, 0.0, 3.0]
      datatype: float64
      shape: [2, 3]

The data section of the coordiantes describe the start end end point in 3D space: [20.0, 0.0, 3.0] to [330.0, 0.0, 3.0] . Therefor the welded part of the workpiece will extend from 20 mm to 330 mm of the joint. The offset in z-direction is 3 mm from the workpiece bottom.

We can create a CoordinateSystemManager instance and add the movement of the welding TCP to the geometry plot:

csm_base = build_base_csm(wx_file, plot=False)
csm_base.plot(
    reference_system="workpiece",
    coordinate_systems=["TCP weld"],
    data_sets=["workpiece (simple)"],
    colors=cs_colors,
    show_wireframe=True,
    show_data_labels=False,
    show_vectors=False,
)
ax_setup(plt.gca())

5. Process description

The arc welding process must be defined using the following properties:

process:
  type: object
  properties:
    welding_process:
      $ref: "asdf://weldx.bam.de/weldx/schemas/process/GMAW-0.1.0"
    shielding_gas:
      tag: "asdf://weldx.bam.de/weldx/tags/aws/process/shielding_gas_for_procedure-0.1.*"
    weld_speed:
      ...
    welding_wire:
      ...
  required: [welding_process, shielding_gas, weld_speed, welding_wire]

We can store the process property in a new variable:

process = wx_file["process"]

The weld speed is restricted to a constant value of dimension “[length]/[time]”

weld_speed:
  tag: "asdf://weldx.bam.de/weldx/tags/core/time_series-0.1.*"
  wx_unit: "m/s"
  wx_shape: [1]
process["weld_speed"]

The welding wire is described by the constant diameter and a string describing the classification.

welding_wire:
  type: object
  properties:
    diameter:
      description: |
        The diameter of the welding wire.
      tag: "asdf://weldx.bam.de/weldx/tags/units/quantity-0.1.*"
      wx_unit: "m"
      wx_shape: [1]
    class:
      type: string

Additional metadata can be stored in the wx_user field if necessary. In the example, a G4Si1 wire with 1.2 mm diameter was used. The manufacturer and charge number are also given.

process["welding_wire"]

The shielding gas information consists of a common name and the gas mixture.

pprint(process["shielding_gas"].torch_shielding_gas.__dict__)
# switch to static plots
%matplotlib inline

The welding_process describes the parameters set at the welding power source during the course of the experiment.

Parameters are represented by a TimeSeries object and can vary over time. In this example, all parameters are set to constant values.

gmaw_process = process["welding_process"]
fig, ax = plot_gmaw(gmaw_process, t)

6. Measurements

We can list all measurements stored in the file.

for measurement in wx_file["measurements"]:
    print(measurement.name)

We can also create a plot showing all signals stored in the measurement chains listed under measurements.

In the example dataset the welding current and voltage are recorded during the welding process. The temperature-measurements are recorded before and after the welding experiment. Due to the use of reference times, all signals can be synchronized.

plot_measurements(wx_file["measurements"])
# switch to interactive plots
%matplotlib widget

Plot the voltage measurement:

plot_signal(wx_file["welding_voltage"], name="welding_voltage")

Plot the current meausurement:

plot_signal(wx_file["welding_current"], name="welding_current")

Here is a detailed look at the current waveform:

plot_signal(wx_file["welding_current"], name="welding_current", limits=(23, 23.025))

7. Measurement chains

To document how a welding related measurement was conducted, we can describe and store measurement chains using the weldx API. This includes:

  • describing the measurement equipment

  • describing multiple transformation steps from raw-data to the final measurement

  • providing information about measurement uncertainties and errors

  • attaching certification examples or similar files

An in depth example describing measurement chains can be found in the documentation: meassurement_example

current measurement chain

current_measurement_chain = wx_file["measurements"][0].measurement_chain

current_source = current_measurement_chain.source
print(current_source.name)
for processor in current_measurement_chain.transformations:
    print(processor.name)

Each measurement chain object can be visualized with it’s plot function. The squared nodes represent signals, the circular nodes show data that is present for a single signal. Transformation steps between signals are given with name and some info.

# switch to static plots
%matplotlib inline
fig, ax = plt.subplots(nrows=1, figsize=(12, 6))
wx_file["measurements"][0].measurement_chain.plot(ax);
fig, ax = plt.subplots(nrows=1, figsize=(12, 6))
wx_file["measurements"][1].measurement_chain.plot(ax);
fig, ax = plt.subplots(nrows=1, figsize=(12, 6))
wx_file["measurements"][2].measurement_chain.plot(ax);

8. coordinate systems

The weldx API contains multiple functions to describe dependencies and transformations between multiple different coordinate systems.

  • translations and rotations

  • constant and time dependent transformations

  • transformation between different systems

  • grouping multiple systems into subsystems

  • transforming spatial data between different coordinate systems

  • visualization of transformations and systems

There are multiple tutorials available covering coordinate transformations using the LocalCoordinateSystem and CoordinateSystemManager classes:

  • https://weldx.readthedocs.io/en/v0.5.0/tutorials/transformations_01_coordinate_systems.html

  • https://weldx.readthedocs.io/en/v0.5.0/tutorials/transformations_02_coordinate_system_manager.html

  • https://weldx.readthedocs.io/en/v0.5.0/tutorials/transformations_02_coordinate_system_manager.html#Visualizing-the-coordinate-systems-of-the-CSM

  • https://weldx.readthedocs.io/en/v0.5.0/tutorials/welding_example_02_weaving.html

# switch to static plots
%matplotlib inline

In addition to the simplified weldment specification, the example dataset contains the complete coordinate system information describing the BAM arc welding setup.

  • the definition of the reference user frame used for robot programming

  • the recorded actual TCP movement of the robot

  • the movement of a laser line scanner attached to the robot head

We can load the instance of the coordinate system manager directly from the weldx file.
Following the file schema the data can be accessed under the key coordinate_systems.

csm = wx_file["coordinate_systems"]

We can visualize all loaded coordinate systems using the built-in plot functions.

csm
csm.plot_graph()
plt.gcf().set_size_inches(w=6, h=6)

Let’s take another look at the weld specimen.

The workpiece coordinate system has it’s origin located at the start of the workpiece at groove center. We can calculate the position of the thermocouple placement in the workpiece coordinate system.

csm.get_cs("T1", "user_frame")

The second thermocouple is offset by 5 mm from the first.

csm.get_cs("T2", "T1")

The following command will calculate the recorded robot TCP movement in reference to the workpiece coordinate system. Since the robot movement is time dependent, the result will be a time dependent coordiante system.

csm.get_cs("TCP", "workpiece")
csm

9. Interactive 3D visualization

Add geometry data to CSM

For visualization using the k3d backend we create some 3D data from the workpiece description metadata and add it to the CoordinateSystemManager

geometry_full_width = create_geometry(groove, seam_length, Q_(100, "mm"))
spatial_data_geo_full = geometry_full_width.spatial_data(
    profile_raster_width=Q_(4, "mm"), trace_raster_width=Q_(60, "mm")
)
spatial_data_geo_full.coordinates = spatial_data_geo_full.coordinates.astype("float32")

spatial_data_geo_reduced = geometry.spatial_data(
    profile_raster_width=Q_(4, "mm"), trace_raster_width=Q_(60, "mm")
)

csm.assign_data(spatial_data_geo_full, "workpiece geometry", "workpiece")
csm.assign_data(spatial_data_geo_reduced, "workpiece geometry (reduced)", "workpiece")

We also add a 3D model of the welding wire and attach it to the TCP coordinate system. The wire model will follow the TCP movement.

welding_wire_diameter = wx_file["process"]["welding_wire"]["diameter"].m
csm.assign_data(
    welding_wire_geo_data(welding_wire_diameter / 2, 17, 16), "welding_wire", "TCP"
)

Here is an example visualization of the experiment design.
The reconstruction is entierly based on the metadata stored inside the weldx file.

csm.plot(
    reference_system="workpiece",
    coordinate_systems=["TCP design", "T1", "T2"],
    data_sets=["workpiece geometry", "welding_wire"],
    colors=cs_colors,
    show_data_labels=True,
    backend="k3d",
)

Here is the same visualization, this time using only actual measurement data for the plot.

The TCP motion is taken from the robot recording of the actual TCP during welding operation.
The workpiece data was obtained from a 3D scan of the workpiece data before and after welding. We can switch the pre- and post-weld scan data for the plot by selecting the corresponding datasets scan_0 or scan_1 in the following cell.

csm.plot(
    reference_system="workpiece",
    coordinate_systems=["TCP", "T1", "T2"],
    data_sets=["scan_0", "scan_1", "welding_wire"],
    colors=cs_colors,
    show_data_labels=True,
    backend="k3d",
)