Repository: magpylib/magpylib (language: python)

Issue:
The Collection-Compound Class
from #434 

The idea is to enable (but not add) the Compound class which refers to all classes that inherit BaseGeo AND Collection. This enables "tracking" of Collection objects that could be Halbach Arrays or similar magnet compounds.

For this to work the move/rotate operations have to be rewritten so that they do not overwrite each other but can apply to BaseGeo and Collection objects at the same time.

When `display(path='True')` only the compound path should be visible by default - how to turn on all paths ?


- [x] #443
- [x] fix display methods - how to turn all paths on and off ?
- [x] #446 
- [x] fix tests
- [x] do extended manual testing (@OrtnerMichael)
- [x] update docstrings (edit `Collection` docstring with Compound properties)
- [x] update changelog
- [x] update docs
- [x] #450
- [x] choose N!=M behavior
The Collection-Compound Class
from #434 

The idea is to enable (but not add) the Compound class which refers to all classes that inherit BaseGeo AND Collection. This enables "tracking" of Collection objects that could be Halbach Arrays or similar magnet compounds.

For this to work the move/rotate operations have to be rewritten so that they do not overwrite each other but can apply to BaseGeo and Collection objects at the same time.

When `display(path='True')` only the compound path should be visible by default - how to turn on all paths ?


- [x] #443
- [x] fix display methods - how to turn all paths on and off ?
- [x] #446 
- [x] fix tests
- [x] do extended manual testing (@OrtnerMichael)
- [x] update docstrings (edit `Collection` docstring with Compound properties)
- [x] update changelog
- [x] update docs
- [x] #450
- [x] choose N!=M behavior


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b893b729..00e31023e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,7 +26,9 @@ This is a major update that includes
     - Added `matplotlib` pixel display ([#279](https://github.com/magpylib/magpylib/issues/279))
 - Documentation and Example codes now available on read the docs with binder links and live code. ([#389](https://github.com/magpylib/magpylib/issues/389))
 - `Collection` objects can now be composed of `sources`, `sensors` or both types. The `getB` and `getH` functions accommodate for all cases. ([#410](https://github.com/magpylib/magpylib/issues/410), [#415](https://github.com/magpylib/magpylib/pull/415), [#297](https://github.com/magpylib/magpylib/issues/297))
-- Add all scipy Rotation forms as object methods ([#427](https://github.com/magpylib/magpylib/pull/427))
+- `Collection` has it's own `position`, `orientation` and `style`. This is useful to build compound objects. ([#444](https://github.com/magpylib/magpylib/issues/444))
+- All scipy Rotation forms are now also object methods ([#427](https://github.com/magpylib/magpylib/pull/427))
+- The new `absolute` argument for the `move` method enables the absolute positioning in the global cooradinate system ([#444](https://github.com/magpylib/magpylib/issues/444))
 
 
 ### Changed
@@ -42,6 +44,7 @@ This is a major update that includes
   - `show_path` ➡️ `path`
   - `size_sensors`&`size_dipoles` ➡️ `style_size`
   - `size_direction` ➡️ `style_magnetization_size`
+- `move` and `rotate` methods have `start='auto'` instead of `start=-1`. Apply fullpath if input is a scalar, append/merge if input is a vector ([#438](https://github.com/magpylib/magpylib/discussions/438), [#444](https://github.com/magpylib/magpylib/issues/444))
 
 ### Updated
 - Computation:
@@ -64,6 +67,7 @@ This is a major update that includes
 
 ### Removed
 - `config.ITERCYLINDER` is now obsolete. The iterative solution replaced by a new analytical expression.
+-  removed `increment` argument from `move` and `rotate` functions ([#438](https://github.com/magpylib/magpylib/discussions/438), [#444](https://github.com/magpylib/magpylib/issues/444))
 
 
 ---
diff --git a/docs/_codes/doc_collection_display.py b/docs/_codes/doc_collection_display.py
index 8176abdb6..b8ce9fff2 100644
--- a/docs/_codes/doc_collection_display.py
+++ b/docs/_codes/doc_collection_display.py
@@ -1,3 +1,4 @@
+import numpy as np
 import magpylib as magpy
 
 # create a Collection of three sources
@@ -6,9 +7,9 @@
 col = s1 + s2
 
 # generate a spiral path
-s1.move([(.2,0,0)]*100, increment=True)
-s2.move([(-.2,0,0)]*100, increment=True)
-col.rotate_from_angax([5]*100, 'z', anchor=0, increment=True, start=0)
+s1.move(np.linspace((.2,0.,0.),(20.,0.,0.),100), start=0)
+s2.move(np.linspace((-.2,0.,0.),(-20.,0.,0.),100), start=0)
+col.rotate_from_angax(np.linspace(5.,500.,100), 'z', anchor=0, start=0)
 
 # display
-col.display(zoom=-.3, path=10)
+magpy.display(*col, zoom=-.3, path=10)
diff --git a/docs/_pages/0_documentation.md b/docs/_pages/0_documentation.md
index 06ef2060b..88933690c 100644
--- a/docs/_pages/0_documentation.md
+++ b/docs/_pages/0_documentation.md
@@ -195,6 +195,7 @@ for src in col:
 When all source and sensor objects are created and all paths are defined `display` (top level function and method of all Magpylib objects) provides a convenient way to graphically view the geometric arrangement through Matplotlib.
 
 ```python
+import numpy as np
 import magpylib as magpy
 
 # create a Collection of three sources
@@ -203,12 +204,13 @@ s2 = magpy.magnet.Cuboid(magnetization=(0,0,100), dimension=(2,2,2), position=(-
 col = s1 + s2
 
 # generate a spiral path
-s1.move([(.2,0,0)]*100, increment=True)
-s2.move([(-.2,0,0)]*100, increment=True)
-col.rotate_from_angax([5]*100, 'z', anchor=0, increment=True, start=0)
+s1.move(np.linspace((.2,0.,0.),(20.,0.,0.),100), start=0)
+s2.move(np.linspace((-.2,0.,0.),(-20.,0.,0.),100), start=0)
+col.rotate_from_angax(np.linspace(5.,500.,100), 'z', anchor=0, start=0)
 
 # display
-col.display(path=10)
+magpy.display(*col, zoom=-.3, path=10)
+
 ```
 
 ```{eval-rst}
diff --git a/docs/examples/plot_01_basic_functionality.md b/docs/examples/plot_01_basic_functionality.md
index 58d14cecf..805beb194 100644
--- a/docs/examples/plot_01_basic_functionality.md
+++ b/docs/examples/plot_01_basic_functionality.md
@@ -34,13 +34,13 @@ In this example the field B in \[mT\] of the Cylinder magnet is evaluated for a
 rotating 360° with 45° steps around the source along the z-axis and a radius of 5\[mm\].
 
 ```{code-cell} ipython3
-from magpylib.magnet import Cylinder
+import numpy as np
 from magpylib import Sensor
+from magpylib.magnet import Cylinder
 
-src = Cylinder(magnetization=(222, 333, 444), dimension=(2, 2))
-sens = Sensor(position=(5, 0, 0)).rotate_from_angax(
-    [45] * 8, "z", anchor=(0, 0, 0), start=0, increment=True
-)
+cyl = Cylinder(magnetization=(222, 333, 444), dimension=(2, 2))
+sens = Sensor(position=(5, 0, 0))
+sens.rotate_from_angax(np.linspace(0.0, 270, 12)[1:], "z", anchor=cyl.position)
 B = sens.getB(src)
 print(B)
 ```
diff --git a/docs/examples/plot_02_display_backends.md b/docs/examples/plot_02_display_backends.md
index b1cb614cf..129c5d725 100644
--- a/docs/examples/plot_02_display_backends.md
+++ b/docs/examples/plot_02_display_backends.md
@@ -34,13 +34,14 @@ magpy.display(magnet, sens, zoom=1)
 Display figure on your own canvas (here Matplotlib 3D axis):
 
 ```{code-cell} ipython3
+import numpy as np
 import matplotlib.pyplot as plt
 import magpylib as magpy
 
 my_axis = plt.axes(projection="3d")
 src = magpy.magnet.Sphere(magnetization=(0, 0, 1), diameter=1)
-src.move([(0.1, 0, 0)] * 50, increment=True)
-src.rotate_from_angax(angle=[10] * 50, axis="z", anchor=0, start=0, increment=True)
+src.move(np.linspace((0.1, 0, 0), (5,0,0), 50))
+src.rotate_from_angax(angle=np.linspace(10, 500, 50), axis="z", anchor=0, start=1)
 ts = [-0.4, 0, 0.4]
 sens = magpy.Sensor(position=(0, 0, 2), pixel=[(x, y, 0) for x in ts for y in ts])
 magpy.display(src, sens, canvas=my_axis)
@@ -50,14 +51,15 @@ plt.show()
 The same objects can also be displayed using the `plotly` plotting backend
 
 ```{code-cell} ipython3
+import numpy as np
 import magpylib as magpy
 
 src = magpy.magnet.Sphere(magnetization=(0, 0, 1), diameter=1)
-src.move([(0.1, 0, 0)] * 50, increment=True)
-src.rotate_from_angax(angle=[10] * 50, axis="z", anchor=0, start=0, increment=True)
+src.move(np.linspace((0.1, 0, 0), (5,0,0), 50))
+src.rotate_from_angax(angle=np.linspace(10, 500, 50), axis="z", anchor=0, start=1)
 ts = [-0.4, 0, 0.4]
 sens = magpy.Sensor(position=(0, 0, 2), pixel=[(x, y, 0) for x in ts for y in ts])
-magpy.display(src, backend="plotly")
+magpy.display(src, sens, backend="plotly")
 ```
 
 The display function is also available as a class method and can be called for every object separately.
@@ -72,7 +74,3 @@ sens = magpy.Sensor(position=(0, 0, 2), pixel=[(x, y, 0) for x in ts for y in ts
 sens.display(canvas=fig, backend="plotly", zoom=1, style_size=5)
 fig
 ```
-
-```{note}
-Displaying figures with the `plotly` backend does not necessarily need to provide a canvas. It is shown here only as an example.
-```
diff --git a/docs/examples/plot_03_display_styling.md b/docs/examples/plot_03_display_styling.md
index 0aac12ee0..63d91666c 100644
--- a/docs/examples/plot_03_display_styling.md
+++ b/docs/examples/plot_03_display_styling.md
@@ -139,12 +139,12 @@ magpy.display(col, backend="plotly")
 
 ```{note}
 The `Collection` object does not hold any `style` attribute on its own but the helper method
-`set_styles` allows setting the style of all its children where the set arguments match
+`set_children_styles` allows setting the style of all its children where the set arguments match
 existing child style attributes.
 ```
 
 ```{code-cell} ipython3
-col.set_styles(magnetization_color_south="blue")
+col.set_children_styles(magnetization_color_south="blue")
 
 magpy.display(col, backend="plotly")
 ```
diff --git a/docs/examples/plot_04_animate_paths.md b/docs/examples/plot_04_animate_paths.md
index 0b736cbc8..b53371758 100644
--- a/docs/examples/plot_04_animate_paths.md
+++ b/docs/examples/plot_04_animate_paths.md
@@ -36,33 +36,30 @@ import magpylib as magpy
 
 # Define sources and sensor
 dim = 2
-col = magpy.Collection(
-    [
-        magpy.magnet.Cuboid(magnetization=(0, 1, 0), dimension=[dim] * 3),
-        magpy.magnet.Cylinder(magnetization=(0, 1, 0), dimension=[dim] * 2),
-        magpy.magnet.Sphere(magnetization=(0, 1, 0), diameter=dim),
-    ]
+coll = magpy.Collection(
+    magpy.magnet.Cuboid(magnetization=(0, 1, 0), dimension=[dim] * 3),
+    magpy.magnet.Cylinder(magnetization=(0, 1, 0), dimension=[dim] * 2),
+    magpy.magnet.Sphere(magnetization=(0, 1, 0), diameter=dim),
 )
 
 ts = np.arange(-0.6, 0.6, 0.2)
 sens = magpy.Sensor(position=(0, 0, 2), pixel=[(x, y, 0) for x in ts for y in ts])
 
 # Create paths
-pathlen = 40
-for displ, src in zip([(0.1414, 0, 0.1), (-0.1, -0.1, 0.1), (-0.1, 0.1, 0.1)], col):
-    src.move([np.array(displ)] * pathlen, increment=True)
-    src.rotate_from_angax(
-        angle=[10] * pathlen, axis="z", anchor=0, start=0, increment=True
-    )
-sens.move([(0, 0, 0.1)] * 10, increment=True)
+pathlen = 50
+start_positions = np.array([(1.414, 0, 1), (-1, -1, 1), (-1, 1, 1)])
+for pos, src in zip(start_positions, coll):
+    src.move(np.linspace(pos, pos*pathlen*0.1, pathlen), start=0)
+    src.rotate_from_angax(np.linspace(0., 360., pathlen), 'z', anchor=0, start=0)
+sens.move(np.linspace((0.,0.,5.), (0.,0.,-5.), 20), start=0)
 
 # display animation
 fig = go.Figure()
 magpy.display(
-    col,
+    *coll,
     sens,
     canvas=fig,
-    path="animate",
+    path='animate',
     zoom=0,
     animate_time=2,
     animate_fps=20,
@@ -75,5 +72,5 @@ fig.update_layout(height=800)
 ```{warning}
 Even if some failsafes are implemented such as a maximum frame rate and frame count. There is no
 guarantee that the animation will be able to be rendered. This is particularly relevant if the
-user tries to animate many objects at the same time.
+user tries to animate many objects and/or many path positions at the same time.
 ```
diff --git a/docs/examples/plot_08_complex_extra_model3d.md b/docs/examples/plot_08_complex_extra_model3d.md
index b27d084a1..1438f6512 100644
--- a/docs/examples/plot_08_complex_extra_model3d.md
+++ b/docs/examples/plot_08_complex_extra_model3d.md
@@ -29,25 +29,31 @@ import plotly.graph_objects as go
 import requests
 from stl import mesh  # needs numpy-stl installed
 
+
 def get_stl_color(x, return_rgb_string=True):
     sb = f"{x:015b}"[::-1]
     r = int(255 / 31 * int(sb[:5], base=2))
     g = int(255 / 31 * int(sb[5:10], base=2))
     b = int(255 / 31 * int(sb[10:15], base=2))
     if return_rgb_string:
-        color = f'rgb({r},{g},{b})'
+        color = f"rgb({r},{g},{b})"
     else:
-        color = (r,g,b)
+        color = (r, g, b)
     return color
 
+
 # define stl to mesh3d function
-def stl2mesh3d(stl_file, recenter=False, backend='matplotlib'):
+def stl2mesh3d(stl_file, recenter=False, backend="matplotlib"):
+    """
+    an array of faces/triangles is read by nympy-stl from a stl file;  
+    this function extracts the unique vertices and the triangulation values and
+    returns depending on the backend the corresponding dictionary for further
+    magpylib use as extra 3d-model.
+    The array stl_mesh.vectors.reshape(p*q, r) can contain multiple copies of
+    the same vertex; only unique vertices are extracted from all mesh triangles
+    """
     stl_mesh = mesh.Mesh.from_file(stl_file)
-    # stl_mesh is read by nympy-stl from a stl file; it is  an array of faces/triangles
-    # this function extracts the unique vertices and the lists I, J, K to define a Plotly mesh3d
     p, q, r = stl_mesh.vectors.shape  # (p, 3, 3)
-    # the array stl_mesh.vectors.reshape(p*q, r) can contain multiple copies of the same vertex;
-    # extract unique vertices from all mesh triangles
     vertices, ixr = np.unique(
         stl_mesh.vectors.reshape(p * q, r), return_inverse=True, axis=0
     )
@@ -66,10 +72,20 @@ def stl2mesh3d(stl_file, recenter=False, backend='matplotlib'):
             coordsargs={"x": "args[0]", "y": "args[1]", "z": "args[2]"},
         )
     elif backend == "plotly":
-        facecolor = np.array([get_stl_color(x, return_rgb_string=True) for x in colors]).T
+        facecolor = np.array(
+            [get_stl_color(x, return_rgb_string=True) for x in colors]
+        ).T
         model3d.update(
             trace=dict(
-                type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k, facecolor=facecolor
+                type="mesh3d",
+                x=x,
+                y=y,
+                z=z,
+                i=i,
+                j=j,
+                k=k,
+                facecolor=facecolor
+                # coordsargs is by default: {"x": "x", "y": "y", "z": "z"}
             ),
         )
     else:
@@ -94,18 +110,17 @@ with tempfile.TemporaryDirectory() as tmpdirname:
 # create sensor, add extra 3d-model, create path
 sensor = magpy.Sensor(position=(-15, 0, -6))
 sensor.style = dict(model3d_extra=[model3d_matplotlib, model3d_plotly])
-sensor.rotate_from_angax([5] * 30, "z", increment=True, anchor=(0, 0, 0))
-sensor.move([[0, 0, 0.5]] * 30, increment=True, start=0)
-
+sensor.rotate_from_angax(np.linspace(0, 150, 33), "z", anchor=0, start=0)
+sensor.move(np.linspace((0, 0, 0), (0, 0, 15), 33), start=0)
 # create source, and Collection
 cuboid = magpy.magnet.Cylinder(magnetization=(0, 0, 1000), dimension=(20, 30))
 collection = sensor + cuboid
 
 # display animated system with matplotlib backend
-magpy.display(collection, path=8, style_magnetization_size=0.4, backend="matplotlib")
+magpy.display(*collection, path=8, style_magnetization_size=0.4, backend="matplotlib")
 
 # display animated system with plotly backend
-magpy.display(collection, path=8, backend="plotly")
+magpy.display(*collection, path=8, backend="plotly")
 ```
 
 
diff --git a/magpylib/__init__.py b/magpylib/__init__.py
index 9ce7c972d..2e1411b99 100644
--- a/magpylib/__init__.py
+++ b/magpylib/__init__.py
@@ -318,7 +318,7 @@
     '__author__', '__credits__']
 
 # create interface to outside of package
-from magpylib._src.default_classes import default_settings as defaults
+from magpylib._src.defaults.defaults_classes import default_settings as defaults
 from magpylib import magnet
 from magpylib import current
 from magpylib import misc
diff --git a/magpylib/_src/defaults/__init__.py b/magpylib/_src/defaults/__init__.py
new file mode 100644
index 000000000..44cfafc46
--- /dev/null
+++ b/magpylib/_src/defaults/__init__.py
@@ -0,0 +1,1 @@
+"""defaults module"""
diff --git a/magpylib/_src/default_classes.py b/magpylib/_src/defaults/defaults_classes.py
similarity index 99%
rename from magpylib/_src/default_classes.py
rename to magpylib/_src/defaults/defaults_classes.py
index f79da4d10..1b26bfe87 100644
--- a/magpylib/_src/default_classes.py
+++ b/magpylib/_src/defaults/defaults_classes.py
@@ -1,4 +1,4 @@
-from magpylib._src.default_utils import (
+from magpylib._src.defaults.defaults_utility import (
     MagicProperties,
     validate_property_class,
     color_validator,
diff --git a/magpylib/_src/default_utils.py b/magpylib/_src/defaults/defaults_utility.py
similarity index 99%
rename from magpylib/_src/default_utils.py
rename to magpylib/_src/defaults/defaults_utility.py
index 54acf4edf..615e2c212 100644
--- a/magpylib/_src/default_utils.py
+++ b/magpylib/_src/defaults/defaults_utility.py
@@ -2,7 +2,7 @@
 
 from copy import deepcopy
 import collections.abc
-from magpylib._src.defaults import DEFAULTS
+from magpylib._src.defaults.defaults_values import DEFAULTS
 
 
 SUPPORTED_PLOTTING_BACKENDS = ("matplotlib", "plotly")
diff --git a/magpylib/_src/defaults.py b/magpylib/_src/defaults/defaults_values.py
similarity index 100%
rename from magpylib/_src/defaults.py
rename to magpylib/_src/defaults/defaults_values.py
diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py
index 91e33b90b..2e221f840 100644
--- a/magpylib/_src/display/display.py
+++ b/magpylib/_src/display/display.py
@@ -1,34 +1,10 @@
 """ Display function codes"""
 
 import warnings
-from itertools import cycle
-import numpy as np
-import matplotlib.pyplot as plt
 from magpylib._src.utility import format_obj_input, test_path_format
-from magpylib._src.style import get_style
-from magpylib._src.display.disp_utility import MagpyMarkers
-
-from magpylib._src.display.mpl_draw import (
-    draw_directs_faced,
-    draw_faces,
-    draw_markers,
-    draw_path,
-    draw_pixel,
-    draw_sensors,
-    draw_dipoles,
-    draw_circular,
-    draw_line,
-    draw_model3d_extra,
-)
-from magpylib._src.display.disp_utility import (
-    faces_cuboid,
-    faces_cylinder,
-    system_size,
-    faces_sphere,
-    faces_cylinder_section,
-)
-from magpylib._src.input_checks import check_excitations, check_dimensions
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.display.display_matplotlib import display_matplotlib
+from magpylib._src.input_checks import check_dimensions
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 
 
 # ON INTERFACE
@@ -122,13 +98,14 @@ def display(
     """
 
     # flatten input
-    obj_list = format_obj_input(objects)
+    obj_list_flat = format_obj_input(objects, allow='sources+sensors')
+    obj_list_semi_flat = format_obj_input(objects, allow='sources+sensors+collections')
 
     # test if all source dimensions and excitations (if sho_direc=True) have been initialized
-    check_dimensions(obj_list)
+    check_dimensions(obj_list_flat)
 
     # test if every individual obj_path is good
-    test_path_format(obj_list)
+    test_path_format(obj_list_flat)
     check_show_path = (
         isinstance(path, (int, bool))
         or path == "animate"
@@ -150,7 +127,7 @@ def display(
             )
             path = True
         display_matplotlib(
-            *obj_list,
+            *obj_list_semi_flat,
             markers=markers,
             path=path,
             zoom=zoom,
@@ -159,10 +136,10 @@ def display(
         )
     elif backend == "plotly":
         # pylint: disable=import-outside-toplevel
-        from magpylib._src.display.plotly_draw import display_plotly
+        from magpylib._src.display.plotly.plotly_display import display_plotly
 
         display_plotly(
-            *obj_list,
+            *obj_list_semi_flat,
             markers=markers,
             show_path=path,
             zoom=zoom,
@@ -170,177 +147,3 @@ def display(
             animate_time=animate_time,
             **kwargs,
         )
-
-
-def display_matplotlib(
-    *obj_list,
-    axis=None,
-    markers=None,
-    path=True,
-    zoom=0,
-    color_sequence=None,
-    **kwargs,
-):
-    """
-    Display objects and paths graphically with the matplotlib backend.
-
-    - axis: matplotlib axis3d object
-    - markers: list of marker positions
-    - path: bool / int / list of ints
-    - zoom: zoom level, 0=tight boundaries
-    - color_sequence: list of colors for object coloring
-    """
-    # pylint: disable=protected-access
-    # pylint: disable=too-many-branches
-    # pylint: disable=too-many-statements
-
-    # apply config default values if None
-    if color_sequence is None:
-        color_sequence = Config.display.colorsequence
-
-    # create or set plotting axis
-    if axis is None:
-        fig = plt.figure(dpi=80, figsize=(8, 8))
-        ax = fig.add_subplot(111, projection="3d")
-        ax.set_box_aspect((1, 1, 1))
-        generate_output = True
-    else:
-        ax = axis
-        generate_output = False
-
-    # draw objects and evaluate system size --------------------------------------
-
-    # draw faced objects and store vertices
-    points = []
-    dipoles_color = []
-    sensors_color = []
-    faced_objects_color = []
-
-    for obj, color in zip(obj_list, cycle(color_sequence)):
-        style = get_style(obj, Config, **kwargs)
-        color = style.color if style.color is not None else color
-        lw = 0.25
-        faces = None
-        if obj.style.model3d.extra:
-            draw_model3d_extra(obj, style, path, ax, color)
-        if obj.style.model3d.show:
-            if obj._object_type == "Cuboid":
-                lw = 0.5
-                faces = faces_cuboid(obj, path)
-            elif obj._object_type == "Cylinder":
-                faces = faces_cylinder(obj, path)
-            elif obj._object_type == "CylinderSegment":
-                faces = faces_cylinder_section(obj, path)
-            elif obj._object_type == "Sphere":
-                faces = faces_sphere(obj, path)
-            elif obj._object_type == "Line":
-                if style.arrow.show:
-                    check_excitations([obj])
-                arrow_size = style.arrow.size if style.arrow.show else 0
-                arrow_width = style.arrow.width
-                points += draw_line(
-                    [obj], path, color, arrow_size, arrow_width, ax
-                )
-            elif obj._object_type == "Loop":
-                if style.arrow.show:
-                    check_excitations([obj])
-                arrow_width = style.arrow.width
-                arrow_size = style.arrow.size if style.arrow.show else 0
-                points += draw_circular(
-                    [obj], path, color, arrow_size, arrow_width, ax
-                )
-            elif obj._object_type == "Sensor":
-                sensors_color += [color]
-                points += draw_pixel(
-                    [obj],
-                    ax,
-                    color,
-                    style.pixel.color,
-                    style.pixel.size,
-                    style.pixel.symbol,
-                    path,
-                )
-            elif obj._object_type == "Dipole":
-                dipoles_color += [color]
-                points += [obj.position]
-            elif obj._object_type == "CustomSource":
-                draw_markers(np.array([obj.position]), ax, color, symbol="*", size=10)
-                name = (
-                    obj.style.name
-                    if obj.style.name is not None
-                    else str(type(obj).__name__)
-                )
-                ax.text(*obj.position, name, horizontalalignment="center")
-                points += [obj.position]
-            if faces is not None:
-                faced_objects_color += [color]
-                alpha = style.opacity
-                pts = draw_faces(faces, color, lw, alpha, ax)
-                points += [np.vstack(pts).reshape(-1, 3)]
-                if style.magnetization.show:
-                    check_excitations([obj])
-                    pts = draw_directs_faced(
-                        [obj], [color], ax, path, style.magnetization.size
-                    )
-                    points += pts
-        if path:
-            marker, line = style.path.marker, style.path.line
-            if style.path.show:
-                points += draw_path(
-                    obj,
-                    color,
-                    marker.symbol,
-                    marker.size,
-                    marker.color,
-                    line.style,
-                    line.width,
-                    ax,
-                )
-
-    # markers -------------------------------------------------------
-    if markers is not None and markers:
-        m = MagpyMarkers()
-        style = get_style(m, Config, **kwargs)
-        markers = np.array(markers)
-        s = style.marker
-        draw_markers(markers, ax, s.color, s.symbol, s.size)
-        points += [markers]
-
-    # draw direction arrows (based on src size) -------------------------
-    # objects with faces
-
-    # determine system size -----------------------------------------
-    limx1, limx0, limy1, limy0, limz1, limz0 = system_size(points)
-
-    # make sure ranges are not null
-    limits = np.array([[limx0, limx1], [limy0, limy1], [limz0, limz1]])
-    limits[np.squeeze(np.diff(limits)) == 0] += np.array([-1, 1])
-    sys_size = np.max(np.diff(limits))
-    c = limits.mean(axis=1)
-    m = sys_size.max() / 2
-    ranges = np.array([c - m * (1 + zoom), c + m * (1 + zoom)]).T
-
-    # draw all system sized based quantities -------------------------
-    # sensors
-    sensors = [obj for obj in obj_list if obj._object_type == "Sensor"]
-
-    # dipoles
-    dipoles = [obj for obj in obj_list if obj._object_type == "Dipole"]
-
-    # not optimal for loop if many sensors/dipoles
-    for sensor in sensors:
-        style = get_style(sensor, Config, **kwargs)
-        draw_sensors([sensor], ax, sys_size, path, style.size)
-    for dipole, color in zip(dipoles, dipoles_color):
-        style = get_style(dipole, Config, **kwargs)
-        draw_dipoles([dipole], ax, sys_size, path, style.size, color, style.pivot)
-
-    # plot styling --------------------------------------------------
-    ax.set(
-        **{f"{k}label": f"{k} [mm]" for k in "xyz"},
-        **{f"{k}lim": r for k, r in zip("xyz", ranges)},
-    )
-
-    # generate output ------------------------------------------------
-    if generate_output:
-        plt.show()
diff --git a/magpylib/_src/display/mpl_draw.py b/magpylib/_src/display/display_matplotlib.py
similarity index 55%
rename from magpylib/_src/display/mpl_draw.py
rename to magpylib/_src/display/display_matplotlib.py
index aa23298d5..bf659b586 100644
--- a/magpylib/_src/display/mpl_draw.py
+++ b/magpylib/_src/display/display_matplotlib.py
@@ -1,15 +1,24 @@
 """ matplotlib draw-functionalities"""
 
+from itertools import cycle
 import numpy as np
+import matplotlib.pyplot as plt
 from mpl_toolkits.mplot3d.art3d import Poly3DCollection
-from magpylib._src.default_classes import default_settings as Config
-from magpylib._src.display.disp_utility import (
+from magpylib._src.defaults.defaults_classes import default_settings as Config
+from magpylib._src.display.display_utility import (
     get_rot_pos_from_path,
     draw_arrow_from_vertices,
     draw_arrowed_circle,
     place_and_orient_model3d,
+    faces_cuboid,
+    faces_cylinder,
+    system_size,
+    faces_sphere,
+    faces_cylinder_section,
 )
-
+from magpylib._src.input_checks import check_excitations
+from magpylib._src.style import get_style
+from magpylib._src.display.display_utility import MagpyMarkers
 
 def draw_directs_faced(faced_objects, colors, ax, show_path, size_direction):
     """draw direction of magetization of faced magnets
@@ -308,3 +317,183 @@ def draw_model3d_extra(obj, style, show_path, ax, color):
             kwargs.update({k: v for k, v in tr.items() if k not in ("type", "args")})
             args = tr.get("args", [])
             getattr(ax, tr["type"])(*args, **kwargs)
+
+
+def display_matplotlib(
+    *obj_list_semi_flat,
+    axis=None,
+    markers=None,
+    path=True,
+    zoom=0,
+    color_sequence=None,
+    **kwargs,
+):
+    """
+    Display objects and paths graphically with the matplotlib backend.
+
+    - axis: matplotlib axis3d object
+    - markers: list of marker positions
+    - path: bool / int / list of ints
+    - zoom: zoom level, 0=tight boundaries
+    - color_sequence: list of colors for object coloring
+    """
+    # pylint: disable=protected-access
+    # pylint: disable=too-many-branches
+    # pylint: disable=too-many-statements
+
+    # apply config default values if None
+    if color_sequence is None:
+        color_sequence = Config.display.colorsequence
+    # create or set plotting axis
+    if axis is None:
+        fig = plt.figure(dpi=80, figsize=(8, 8))
+        ax = fig.add_subplot(111, projection="3d")
+        ax.set_box_aspect((1, 1, 1))
+        generate_output = True
+    else:
+        ax = axis
+        generate_output = False
+
+    # draw objects and evaluate system size --------------------------------------
+
+    # draw faced objects and store vertices
+    points = []
+    dipoles_color = []
+    sensors_color = []
+    faced_objects_color = []
+
+    for semi_flat_obj, color in zip(obj_list_semi_flat, cycle(color_sequence)):
+        flat_objs = [semi_flat_obj]
+        if getattr(semi_flat_obj, "children", None) is not None:
+            flat_objs = semi_flat_obj.children
+            if getattr(semi_flat_obj, "position", None) is not None:
+                flat_objs += [semi_flat_obj]
+                color = color if semi_flat_obj.style.color is None else semi_flat_obj.style.color
+        for obj in flat_objs:
+            style = get_style(obj, Config, **kwargs)
+            color = style.color if style.color is not None else color
+            lw = 0.25
+            faces = None
+            if obj.style.model3d.extra:
+                draw_model3d_extra(obj, style, path, ax, color)
+            if obj.style.model3d.show:
+                if obj._object_type == "Cuboid":
+                    lw = 0.5
+                    faces = faces_cuboid(obj, path)
+                elif obj._object_type == "Cylinder":
+                    faces = faces_cylinder(obj, path)
+                elif obj._object_type == "CylinderSegment":
+                    faces = faces_cylinder_section(obj, path)
+                elif obj._object_type == "Sphere":
+                    faces = faces_sphere(obj, path)
+                elif obj._object_type == "Line":
+                    if style.arrow.show:
+                        check_excitations([obj])
+                    arrow_size = style.arrow.size if style.arrow.show else 0
+                    arrow_width = style.arrow.width
+                    points += draw_line(
+                        [obj], path, color, arrow_size, arrow_width, ax
+                    )
+                elif obj._object_type == "Loop":
+                    if style.arrow.show:
+                        check_excitations([obj])
+                    arrow_width = style.arrow.width
+                    arrow_size = style.arrow.size if style.arrow.show else 0
+                    points += draw_circular(
+                        [obj], path, color, arrow_size, arrow_width, ax
+                    )
+                elif obj._object_type == "Sensor":
+                    sensors_color += [color]
+                    points += draw_pixel(
+                        [obj],
+                        ax,
+                        color,
+                        style.pixel.color,
+                        style.pixel.size,
+                        style.pixel.symbol,
+                        path,
+                    )
+                elif obj._object_type == "Dipole":
+                    dipoles_color += [color]
+                    points += [obj.position]
+                elif obj._object_type == "CustomSource":
+                    draw_markers(np.array([obj.position]), ax, color, symbol="*", size=10)
+                    name = (
+                        obj.style.name
+                        if obj.style.name is not None
+                        else str(type(obj).__name__)
+                    )
+                    ax.text(*obj.position, name, horizontalalignment="center")
+                    points += [obj.position]
+                if faces is not None:
+                    faced_objects_color += [color]
+                    alpha = style.opacity
+                    pts = draw_faces(faces, color, lw, alpha, ax)
+                    points += [np.vstack(pts).reshape(-1, 3)]
+                    if style.magnetization.show:
+                        check_excitations([obj])
+                        pts = draw_directs_faced(
+                            [obj], [color], ax, path, style.magnetization.size
+                        )
+                        points += pts
+            if path:
+                marker, line = style.path.marker, style.path.line
+                if style.path.show:
+                    points += draw_path(
+                        obj,
+                        color,
+                        marker.symbol,
+                        marker.size,
+                        marker.color,
+                        line.style,
+                        line.width,
+                        ax,
+                    )
+
+    # markers -------------------------------------------------------
+    if markers is not None and markers:
+        m = MagpyMarkers()
+        style = get_style(m, Config, **kwargs)
+        markers = np.array(markers)
+        s = style.marker
+        draw_markers(markers, ax, s.color, s.symbol, s.size)
+        points += [markers]
+
+    # draw direction arrows (based on src size) -------------------------
+    # objects with faces
+
+    # determine system size -----------------------------------------
+    limx1, limx0, limy1, limy0, limz1, limz0 = system_size(points)
+
+    # make sure ranges are not null
+    limits = np.array([[limx0, limx1], [limy0, limy1], [limz0, limz1]])
+    limits[np.squeeze(np.diff(limits)) == 0] += np.array([-1, 1])
+    sys_size = np.max(np.diff(limits))
+    c = limits.mean(axis=1)
+    m = sys_size.max() / 2
+    ranges = np.array([c - m * (1 + zoom), c + m * (1 + zoom)]).T
+
+    # draw all system sized based quantities -------------------------
+    # sensors
+    sensors = [obj for obj in obj_list_semi_flat if obj._object_type == "Sensor"]
+
+    # dipoles
+    dipoles = [obj for obj in obj_list_semi_flat if obj._object_type == "Dipole"]
+
+    # not optimal for loop if many sensors/dipoles
+    for sensor in sensors:
+        style = get_style(sensor, Config, **kwargs)
+        draw_sensors([sensor], ax, sys_size, path, style.size)
+    for dipole, color in zip(dipoles, dipoles_color):
+        style = get_style(dipole, Config, **kwargs)
+        draw_dipoles([dipole], ax, sys_size, path, style.size, color, style.pivot)
+
+    # plot styling --------------------------------------------------
+    ax.set(
+        **{f"{k}label": f"{k} [mm]" for k in "xyz"},
+        **{f"{k}lim": r for k, r in zip("xyz", ranges)},
+    )
+
+    # generate output ------------------------------------------------
+    if generate_output:
+        plt.show()
diff --git a/magpylib/_src/display/disp_utility.py b/magpylib/_src/display/display_utility.py
similarity index 100%
rename from magpylib/_src/display/disp_utility.py
rename to magpylib/_src/display/display_utility.py
diff --git a/magpylib/_src/display/plotly/__init__.py b/magpylib/_src/display/plotly/__init__.py
new file mode 100644
index 000000000..21c94b0be
--- /dev/null
+++ b/magpylib/_src/display/plotly/__init__.py
@@ -0,0 +1,1 @@
+"""display.plotly"""
diff --git a/magpylib/_src/display/plotly/plotly_base_traces.py b/magpylib/_src/display/plotly/plotly_base_traces.py
new file mode 100644
index 000000000..44ad31155
--- /dev/null
+++ b/magpylib/_src/display/plotly/plotly_base_traces.py
@@ -0,0 +1,209 @@
+"""plotly base traces utility functions"""
+
+import numpy as np
+from magpylib._src.display.plotly.plotly_utility import merge_mesh3d
+
+
+def make_BaseCuboid(dimension=(1.0, 1.0, 1.0), position=(0.0, 0.0, 0.0)) -> dict:
+    """
+    Provides the base plotly cuboid mesh3d parameters in a dictionary based on dimension and
+    position
+    The zero position is in the barycenter of the vertices.
+    """
+    return dict(
+        type="mesh3d",
+        i=np.array([7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7]),
+        j=np.array([0, 7, 1, 2, 6, 7, 1, 2, 5, 5, 2, 2]),
+        k=np.array([3, 4, 2, 3, 5, 6, 5, 5, 0, 1, 7, 6]),
+        x=np.array([-1, -1, 1, 1, -1, -1, 1, 1]) * 0.5 * dimension[0] + position[0],
+        y=np.array([-1, 1, 1, -1, -1, 1, 1, -1]) * 0.5 * dimension[1] + position[1],
+        z=np.array([-1, -1, -1, -1, 1, 1, 1, 1]) * 0.5 * dimension[2] + position[2],
+    )
+
+
+def make_BasePrism(
+    base_vertices=3, diameter=1, height=1, position=(0.0, 0.0, 0.0)
+) -> dict:
+    """
+    Provides the base plotly prism mesh3d parameters in a dictionary based on number of vertices of
+    the base, the diameter the height and position.
+    The zero position is in the barycenter of the vertices.
+    """
+    N = base_vertices
+    t = np.linspace(0, 2 * np.pi, N, endpoint=False)
+    c1 = np.array([1 * np.cos(t), 1 * np.sin(t), t * 0 - 1]) * 0.5
+    c2 = np.array([1 * np.cos(t), 1 * np.sin(t), t * 0 + 1]) * 0.5
+    c3 = np.array([[0, 0], [0, 0], [-1, 1]]) * 0.5
+    c = np.concatenate([c1, c2, c3], axis=1)
+    c = c.T * np.array([diameter, diameter, height]) + np.array(position)
+    i1 = np.arange(N)
+    j1 = i1 + 1
+    j1[-1] = 0
+    k1 = i1 + N
+
+    i2 = i1 + N
+    j2 = j1 + N
+    j2[-1] = N
+    k2 = i1 + 1
+    k2[-1] = 0
+
+    i3 = i1
+    j3 = j1
+    k3 = i1 * 0 + 2 * N
+
+    i4 = i2
+    j4 = j2
+    k4 = k3 + 1
+
+    # k2&j2 and k3&j3 inverted because of face orientation
+    i = np.concatenate([i1, i2, i3, i4])
+    j = np.concatenate([j1, k2, k3, j4])
+    k = np.concatenate([k1, j2, j3, k4])
+
+    x, y, z = c.T
+    return dict(type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k)
+
+
+def make_BaseEllipsoid(
+    dimension=(1.0, 1.0, 1.0),
+    position=(0.0, 0.0, 0.0),
+    Nvert=15,
+    min_vert=3,
+    max_vert=20,
+) -> dict:
+    """
+    Provides the base plotly ellipsoid mesh3d parameters in a dictionary based on number of vertices
+    of the circumference, the 3 dimensions and position.
+    The zero position is in the barycenter of the vertices.
+    `Nvert` will be forced automatically in the range [`min_vert`, `max_vert`]
+    """
+    N = min(max(Nvert, min_vert), max_vert)
+    phi = np.linspace(0, 2 * np.pi, Nvert, endpoint=False)
+    theta = np.linspace(-np.pi / 2, np.pi / 2, Nvert, endpoint=True)
+    phi, theta = np.meshgrid(phi, theta)
+
+    x = np.cos(theta) * np.sin(phi) * dimension[0] * 0.5 + position[0]
+    y = np.cos(theta) * np.cos(phi) * dimension[1] * 0.5 + position[1]
+    z = np.sin(theta) * dimension[2] * 0.5 + position[2]
+
+    x, y, z = x.flatten()[N - 1 :], y.flatten()[N - 1 :], z.flatten()[N - 1 :]
+
+    i1 = [0] * N
+    j1 = np.array([N] + list(range(1, N)), dtype=int)
+    k1 = np.array(list(range(1, N)) + [N], dtype=int)
+
+    i2 = np.concatenate([k1 + i * N for i in range(N - 2)])
+    j2 = np.concatenate([j1 + i * N for i in range(N - 2)])
+    k2 = np.concatenate([j1 + (i + 1) * N for i in range(N - 2)])
+
+    i3 = np.concatenate([k1 + i * N for i in range(N - 2)])
+    j3 = np.concatenate([j1 + (i + 1) * N for i in range(N - 2)])
+    k3 = np.concatenate([k1 + (i + 1) * N for i in range(N - 2)])
+
+    i = np.concatenate([i1, i2, i3])
+    j = np.concatenate([j1, j2, j3])
+    k = np.concatenate([k1, k2, k3])
+
+    return dict(type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k)
+
+
+def make_BaseCylinderSegment(r1=1, r2=2, h=1, phi1=0, phi2=90, Nvert=30) -> dict:
+    """
+    Provides the base plotly CylinderSegment mesh3d parameters in a dictionary based on inner
+    and outer diameters, height, start angle and end angles in degrees.
+    The zero position is in the barycenter of the vertices.
+    """
+    N = Nvert
+    phi = np.linspace(phi1, phi2, N)
+    x = np.cos(np.deg2rad(phi))
+    y = np.sin(np.deg2rad(phi))
+    z = np.zeros(N)
+    c1 = np.array([r1 * x, r1 * y, z + h / 2])
+    c2 = np.array([r2 * x, r2 * y, z + h / 2])
+    c3 = np.array([r1 * x, r1 * y, z - h / 2])
+    c4 = np.array([r2 * x, r2 * y, z - h / 2])
+    x, y, z = np.concatenate([c1, c2, c3, c4], axis=1)
+
+    i1 = np.arange(N - 1)
+    j1 = i1 + N
+    k1 = i1 + 1
+
+    i2 = k1
+    j2 = j1
+    k2 = j1 + 1
+
+    i3 = i1
+    j3 = k1
+    k3 = j1 + N
+
+    i4 = k3 + 1
+    j4 = k3
+    k4 = k1
+
+    i5 = np.array([0, N])
+    j5 = np.array([2 * N, 0])
+    k5 = np.array([3 * N, 3 * N])
+
+    i = np.hstack(
+        [i1, i2, i1 + 2 * N, i2 + 2 * N, i3, i4, i3 + N, i4 + N, i5, i5 + N - 1]
+    )
+    j = np.hstack(
+        [j1, j2, k1 + 2 * N, k2 + 2 * N, j3, j4, k3 + N, k4 + N, j5, k5 + N - 1]
+    )
+    k = np.hstack(
+        [k1, k2, j1 + 2 * N, j2 + 2 * N, k3, k4, j3 + N, j4 + N, k5, j5 + N - 1]
+    )
+
+    return dict(type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k)
+
+
+def make_BaseCone(
+    base_vertices=3, diameter=1, height=1, position=(0.0, 0.0, 0.0)
+) -> dict:
+    """
+    Provides the base plotly Cone mesh3d parameters in a dictionary based on number of vertices of
+    the base, the diameter the height and position.
+    The zero position is in the barycenter of the vertices.
+    """
+    N = base_vertices
+    t = np.linspace(0, 2 * np.pi, N, endpoint=False)
+    c = np.array([np.cos(t), np.sin(t), t * 0 - 1]) * 0.5
+    tp = np.array([[0, 0, 0.5]]).T
+    c = np.concatenate([c, tp], axis=1)
+    c = c.T * np.array([diameter, diameter, height]) + np.array(position)
+    x, y, z = c.T
+
+    i = np.arange(N, dtype=int)
+    j = i + 1
+    j[-1] = 0
+    k = np.array([N] * N, dtype=int)
+    return dict(type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k)
+
+
+def make_BaseArrow(base_vertices=30, diameter=0.3, height=1, pivot="middle") -> dict:
+    """
+    Provides the base plotly 3D Arrow mesh3d parameters in a dictionary based on number of vertices
+    of the base, the diameter the height and position.
+    The zero position is in the barycenter of the vertices.
+    """
+
+    h, d, z = height, diameter, 0
+    if pivot == "tail":
+        z = h / 2
+    elif pivot == "tip":
+        z = -h / 2
+    cone = make_BaseCone(
+        base_vertices=base_vertices,
+        diameter=d,
+        height=d,
+        position=(0.0, 0.0, z + h / 2 - d / 2),
+    )
+    prism = make_BasePrism(
+        base_vertices=base_vertices,
+        diameter=d / 2,
+        height=h - d,
+        position=(0.0, 0.0, z + -d / 2),
+    )
+    arrow = merge_mesh3d(cone, prism)
+
+    return arrow
diff --git a/magpylib/_src/display/plotly_draw.py b/magpylib/_src/display/plotly/plotly_display.py
similarity index 64%
rename from magpylib/_src/display/plotly_draw.py
rename to magpylib/_src/display/plotly/plotly_display.py
index 013c18250..431bc3520 100644
--- a/magpylib/_src/display/plotly_draw.py
+++ b/magpylib/_src/display/plotly/plotly_display.py
@@ -1,8 +1,8 @@
 """ plolty draw-functionalities"""
 # pylint: disable=C0302
+# pylint: disable=too-many-branches
 
 from itertools import cycle, combinations
-from math import log10
 from typing import Tuple
 import warnings
 
@@ -17,356 +17,47 @@
 import numpy as np
 from scipy.spatial.transform import Rotation as RotScipy
 from magpylib import _src
-from magpylib._src.default_classes import default_settings as Config
-from magpylib._src.display.sensor_plotly_mesh import get_sensor_mesh
+from magpylib._src.defaults.defaults_classes import default_settings as Config
+from magpylib._src.display.plotly.plotly_sensor_mesh import get_sensor_mesh
 from magpylib._src.style import (
     get_style,
     LINESTYLES_MATPLOTLIB_TO_PLOTLY,
     SYMBOLS_MATPLOTLIB_TO_PLOTLY,
 )
-from magpylib._src.display.disp_utility import (
+from magpylib._src.display.display_utility import (
     get_rot_pos_from_path,
     MagpyMarkers,
     draw_arrow_from_vertices,
     draw_arrowed_circle,
     place_and_orient_model3d,
 )
-from magpylib._src.default_utils import (
+from magpylib._src.defaults.defaults_utility import (
     SIZE_FACTORS_MATPLOTLIB_TO_PLOTLY,
     linearize_dict,
 )
-from magpylib._src.input_checks import check_excitations
-
-# Defaults
-
-_UNIT_PREFIX = {
-    -24: "y",  # yocto
-    -21: "z",  # zepto
-    -18: "a",  # atto
-    -15: "f",  # femto
-    -12: "p",  # pico
-    -9: "n",  # nano
-    -6: "µ",  # micro
-    -3: "m",  # milli
-    0: "",
-    3: "k",  # kilo
-    6: "M",  # mega
-    9: "G",  # giga
-    12: "T",  # tera
-    15: "P",  # peta
-    18: "E",  # exa
-    21: "Z",  # zetta
-    24: "Y",  # yotta
-}
-
-
-def unit_prefix(number, unit="", precision=3, char_between="") -> str:
-    """
-    displays a number with given unit and precision and uses unit prefixes for the exponents from
-    yotta (y) to Yocto (Y). If the exponent is smaller or bigger, falls back to scientific notation.
-
-    Parameters
-    ----------
-    number : int, float
-        can be any number
-    unit : str, optional
-        unit symbol can be any string, by default ""
-    precision : int, optional
-        gives the number of significant digits, by default 3
-    char_between : str, optional
-        character to insert between number of prefix. Can be " " or any string, if a space is wanted
-        before the unit symbol , by default ""
-
-    Returns
-    -------
-    str
-        returns formatted number as string
-    """
-    digits = int(log10(abs(number))) // 3 * 3 if number != 0 else 0
-    prefix = _UNIT_PREFIX.get(digits, "")
-
-    if prefix == "":
-        digits = 0
-    new_number_str = f"{number / 10 ** digits:.{precision}g}"
-    return f"{new_number_str}{char_between}{prefix}{unit}"
-
-
-def _getIntensity(vertices, axis) -> np.ndarray:
-    """
-    Calculates the intensity values for vertices based on the distance of the vertices to the mean
-    vertices position in the provided axis direction. It can be used for plotting
-    fields on meshes. If `mag` See more infos here:https://plotly.com/python/3d-mesh/
-
-    Parameters
-    ----------
-    vertices : ndarray Nx3
-        the N vertices of the mesh object
-    axis : ndarray 3
-        direction vector
-
-    Returns
-    -------
-    ndarray N
-        returns 1D array of length N
-    """
-    p = np.array(vertices).T
-    pos = np.mean(p, axis=1)
-    m = np.array(axis) / np.linalg.norm(axis)
-    intensity = (p[0] - pos[0]) * m[0] + (p[1] - pos[1]) * m[1] + (p[2] - pos[2]) * m[2]
-    return intensity
-
-
-def _getColorscale(
-    color_transition=0,
-    color_north="#E71111",  # 'red'
-    color_middle="#DDDDDD",  # 'grey'
-    color_south="#00B050",  # 'green'
-) -> list:
-    """
-    Provides the colorscale for a plotly mesh3d trace.
-    The colorscale must be an array containing arrays mapping a normalized value to an rgb, rgba,
-    hex, hsl, hsv, or named color string. At minimum, a mapping for the lowest (0) and highest (1)
-    values are required. For example, `[[0, 'rgb(0,0,255)'], [1,'rgb(255,0,0)']]`.
-    In this case the colorscale is created depending on the north/middle/south poles colors. If the
-    middle color is `None` the colorscale will only have north and south pole colors.
-
-    Parameters
-    ----------
-    color_transition : float, optional
-        A value between 0 and 1. Sets the smoothness of the color transitions from adjacent colors
-        visualization., by default 0.1
-    color_north : str, optional
-        magnetic north pole color , by default None
-    color_middle : str, optional
-        middle between south and north pole color, by default None
-    color_south : str, optional
-        magnetic north pole color , by default None
-
-    Returns
-    -------
-    list
-        returns colorscale as list of tuples
-    """
-    if color_middle is False:
-        colorscale = [
-            [0.0, color_south],
-            [0.5 * (1 - color_transition), color_south],
-            [0.5 * (1 + color_transition), color_north],
-            [1, color_north],
-        ]
-    else:
-        colorscale = [
-            [0.0, color_south],
-            [0.2 - 0.2 * (color_transition), color_south],
-            [0.2 + 0.3 * (color_transition), color_middle],
-            [0.8 - 0.3 * (color_transition), color_middle],
-            [0.8 + 0.2 * (color_transition), color_north],
-            [1.0, color_north],
-        ]
-    return colorscale
-
-
-def make_BaseCuboid(dim=(1.0, 1.0, 1.0), pos=(0.0, 0.0, 0.0)) -> dict:
-    """
-    Provides the base plotly cuboid mesh3d parameters in a dictionary based on dimension and
-    position
-    The zero position is in the barycenter of the vertices.
-    """
-    return dict(
-        type="mesh3d",
-        i=np.array([7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7]),
-        j=np.array([0, 7, 1, 2, 6, 7, 1, 2, 5, 5, 2, 2]),
-        k=np.array([3, 4, 2, 3, 5, 6, 5, 5, 0, 1, 7, 6]),
-        x=np.array([-1, -1, 1, 1, -1, -1, 1, 1]) * 0.5 * dim[0] + pos[0],
-        y=np.array([-1, 1, 1, -1, -1, 1, 1, -1]) * 0.5 * dim[1] + pos[1],
-        z=np.array([-1, -1, -1, -1, 1, 1, 1, 1]) * 0.5 * dim[2] + pos[2],
-    )
-
-
-def make_BasePrism(base_vertices=3, diameter=1, height=1, pos=(0.0, 0.0, 0.0)) -> dict:
-    """
-    Provides the base plotly prism mesh3d parameters in a dictionary based on number of vertices of
-    the base, the diameter the height and position.
-    The zero position is in the barycenter of the vertices.
-    """
-    N = base_vertices
-    t = np.linspace(0, 2 * np.pi, N, endpoint=False)
-    c1 = np.array([1 * np.cos(t), 1 * np.sin(t), t * 0 - 1]) * 0.5
-    c2 = np.array([1 * np.cos(t), 1 * np.sin(t), t * 0 + 1]) * 0.5
-    c3 = np.array([[0, 0], [0, 0], [-1, 1]]) * 0.5
-    c = np.concatenate([c1, c2, c3], axis=1)
-    c = c.T * np.array([diameter, diameter, height]) + np.array(pos)
-    i1 = np.arange(N)
-    j1 = i1 + 1
-    j1[-1] = 0
-    k1 = i1 + N
-
-    i2 = i1 + N
-    j2 = j1 + N
-    j2[-1] = N
-    k2 = i1 + 1
-    k2[-1] = 0
-
-    i3 = i1
-    j3 = j1
-    k3 = i1 * 0 + 2 * N
-
-    i4 = i2
-    j4 = j2
-    k4 = k3 + 1
-
-    # k2&j2 and k3&j3 inverted because of face orientation
-    i = np.concatenate([i1, i2, i3, i4])
-    j = np.concatenate([j1, k2, k3, j4])
-    k = np.concatenate([k1, j2, j3, k4])
-
-    x, y, z = c.T
-    return dict(type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k)
-
-
-def make_Ellipsoid(
-    dim=(1.0, 1.0, 1.0), pos=(0.0, 0.0, 0.0), Nvert=15, min_vert=3, max_vert=20
-) -> dict:
-    """
-    Provides the base plotly ellipsoid mesh3d parameters in a dictionary based on number of vertices
-    of the circumference, the 3 dimensions and position.
-    The zero position is in the barycenter of the vertices.
-    `Nvert` will be forced automatically in the range [`min_vert`, `max_vert`]
-    """
-    N = min(max(Nvert, min_vert), max_vert)
-    phi = np.linspace(0, 2 * np.pi, Nvert, endpoint=False)
-    theta = np.linspace(-np.pi / 2, np.pi / 2, Nvert, endpoint=True)
-    phi, theta = np.meshgrid(phi, theta)
-
-    x = np.cos(theta) * np.sin(phi) * dim[0] * 0.5 + pos[0]
-    y = np.cos(theta) * np.cos(phi) * dim[1] * 0.5 + pos[1]
-    z = np.sin(theta) * dim[2] * 0.5 + pos[2]
-
-    x, y, z = x.flatten()[N - 1 :], y.flatten()[N - 1 :], z.flatten()[N - 1 :]
-
-    i1 = [0] * N
-    j1 = np.array([N] + list(range(1, N)), dtype=int)
-    k1 = np.array(list(range(1, N)) + [N], dtype=int)
 
-    i2 = np.concatenate([k1 + i * N for i in range(N - 2)])
-    j2 = np.concatenate([j1 + i * N for i in range(N - 2)])
-    k2 = np.concatenate([j1 + (i + 1) * N for i in range(N - 2)])
-
-    i3 = np.concatenate([k1 + i * N for i in range(N - 2)])
-    j3 = np.concatenate([j1 + (i + 1) * N for i in range(N - 2)])
-    k3 = np.concatenate([k1 + (i + 1) * N for i in range(N - 2)])
-
-    i = np.concatenate([i1, i2, i3])
-    j = np.concatenate([j1, j2, j3])
-    k = np.concatenate([k1, k2, k3])
-
-    return dict(type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k)
-
-
-def make_BaseCylinderSegment(r1=1, r2=2, h=1, phi1=0, phi2=90, Nvert=30) -> dict:
-    """
-    Provides the base plotly CylinderSegment mesh3d parameters in a dictionary based on inner
-    and outer diameters, height, start angle and end angles in degrees.
-    The zero position is in the barycenter of the vertices.
-    """
-    N = Nvert
-    phi = np.linspace(phi1, phi2, N)
-    x = np.cos(np.deg2rad(phi))
-    y = np.sin(np.deg2rad(phi))
-    z = np.zeros(N)
-    c1 = np.array([r1 * x, r1 * y, z + h / 2])
-    c2 = np.array([r2 * x, r2 * y, z + h / 2])
-    c3 = np.array([r1 * x, r1 * y, z - h / 2])
-    c4 = np.array([r2 * x, r2 * y, z - h / 2])
-    x, y, z = np.concatenate([c1, c2, c3, c4], axis=1)
-
-    i1 = np.arange(N - 1)
-    j1 = i1 + N
-    k1 = i1 + 1
-
-    i2 = k1
-    j2 = j1
-    k2 = j1 + 1
-
-    i3 = i1
-    j3 = k1
-    k3 = j1 + N
-
-    i4 = k3 + 1
-    j4 = k3
-    k4 = k1
-
-    i5 = np.array([0, N])
-    j5 = np.array([2 * N, 0])
-    k5 = np.array([3 * N, 3 * N])
-
-    i = np.hstack(
-        [i1, i2, i1 + 2 * N, i2 + 2 * N, i3, i4, i3 + N, i4 + N, i5, i5 + N - 1]
-    )
-    j = np.hstack(
-        [j1, j2, k1 + 2 * N, k2 + 2 * N, j3, j4, k3 + N, k4 + N, j5, k5 + N - 1]
-    )
-    k = np.hstack(
-        [k1, k2, j1 + 2 * N, j2 + 2 * N, k3, k4, j3 + N, j4 + N, k5, j5 + N - 1]
-    )
-
-    return dict(type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k)
-
-
-def make_BaseCone(base_vertices=3, diameter=1, height=1, pos=(0.0, 0.0, 0.0)) -> dict:
-    """
-    Provides the base plotly Cone mesh3d parameters in a dictionary based on number of vertices of
-    the base, the diameter the height and position.
-    The zero position is in the barycenter of the vertices.
-    """
-    N = base_vertices
-    t = np.linspace(0, 2 * np.pi, N, endpoint=False)
-    c = np.array([np.cos(t), np.sin(t), t * 0 - 1]) * 0.5
-    tp = np.array([[0, 0, 0.5]]).T
-    c = np.concatenate([c, tp], axis=1)
-    c = c.T * np.array([diameter, diameter, height]) + np.array(pos)
-    x, y, z = c.T
-
-    i = np.arange(N, dtype=int)
-    j = i + 1
-    j[-1] = 0
-    k = np.array([N] * N, dtype=int)
-    return dict(type="mesh3d", x=x, y=y, z=z, i=i, j=j, k=k)
-
-
-def make_BaseArrow(base_vertices=30, diameter=0.3, height=1, pivot="middle") -> dict:
-    """
-    Provides the base plotly 3D Arrow mesh3d parameters in a dictionary based on number of vertices
-    of the base, the diameter the height and position.
-    The zero position is in the barycenter of the vertices.
-    """
-
-    h, d, z = height, diameter, 0
-    if pivot == "tail":
-        z = h / 2
-    elif pivot == "tip":
-        z = -h / 2
-    cone = make_BaseCone(
-        base_vertices=base_vertices,
-        diameter=d,
-        height=d,
-        pos=(0.0, 0.0, z + h / 2 - d / 2),
-    )
-    prism = make_BasePrism(
-        base_vertices=base_vertices,
-        diameter=d / 2,
-        height=h - d,
-        pos=(0.0, 0.0, z + -d / 2),
-    )
-    arrow = merge_mesh3d(cone, prism)
-
-    return arrow
+from magpylib._src.input_checks import check_excitations
+from magpylib._src.utility import unit_prefix, format_obj_input
+from magpylib._src.display.plotly.plotly_base_traces import (
+    make_BaseCuboid,
+    make_BaseCylinderSegment,
+    make_BaseEllipsoid,
+    make_BasePrism,
+    # make_BaseCone,
+    make_BaseArrow,
+)
+from magpylib._src.display.plotly.plotly_utility import (
+    merge_mesh3d,
+    merge_traces,
+    getColorscale,
+    getIntensity,
+)
 
 
 def make_Line(
     current=0.0,
     vertices=((-1.0, 0.0, 0.0), (1.0, 0.0, 0.0)),
-    pos=(0.0, 0.0, 0.0),
+    position=(0.0, 0.0, 0.0),
     orientation=None,
     color=None,
     style=None,
@@ -390,7 +81,7 @@ def make_Line(
         vertices = np.array(vertices).T
     if orientation is not None:
         vertices = orientation.apply(vertices.T).T
-    x, y, z = (vertices.T + pos).T
+    x, y, z = (vertices.T + position).T
     line_width = style.arrow.width * SIZE_FACTORS_MATPLOTLIB_TO_PLOTLY["line_width"]
     line = dict(
         type="scatter3d",
@@ -408,7 +99,7 @@ def make_Line(
 def make_Loop(
     current=0.0,
     diameter=1.0,
-    pos=(0.0, 0.0, 0.0),
+    position=(0.0, 0.0, 0.0),
     Nvert=50,
     orientation=None,
     color=None,
@@ -429,7 +120,7 @@ def make_Loop(
     vertices = draw_arrowed_circle(current, diameter, arrow_size, Nvert)
     if orientation is not None:
         vertices = orientation.apply(vertices.T).T
-    x, y, z = (vertices.T + pos).T
+    x, y, z = (vertices.T + position).T
     line_width = style.arrow.width * SIZE_FACTORS_MATPLOTLIB_TO_PLOTLY["line_width"]
     circular = dict(
         type="scatter3d",
@@ -444,22 +135,24 @@ def make_Loop(
     return {**circular, **kwargs}
 
 
-def make_UnsupportedObject(
-    pos=(0.0, 0.0, 0.0), orientation=None, color=None, style=None, **kwargs,
+def make_DefaultTrace(
+    obj, position=(0.0, 0.0, 0.0), orientation=None, color=None, style=None, **kwargs,
 ) -> dict:
     """
     Creates the plotly scatter3d parameters for an object with no specifically supported
     representation. The object will be reprensented by a scatter point and text above with object
     name.
     """
+
+    default_suffix = ""
     name, name_suffix = get_name_and_suffix(
-        "Unknown object", " (Unsupported visualisation)", style
+        f"{type(obj).__name__}", default_suffix, style
     )
-    vertices = np.array([pos])
+    vertices = np.array([position])
     if orientation is not None:
         vertices = orientation.apply(vertices).T
     x, y, z = vertices
-    obj = dict(
+    trace = dict(
         type="scatter3d",
         x=x,
         y=y,
@@ -471,12 +164,12 @@ def make_UnsupportedObject(
         marker_color=color,
         marker_symbol="diamond",
     )
-    return {**obj, **kwargs}
+    return {**trace, **kwargs}
 
 
 def make_Dipole(
     moment=(0.0, 0.0, 1.0),
-    pos=(0.0, 0.0, 0.0),
+    position=(0.0, 0.0, 0.0),
     orientation=None,
     style=None,
     autosize=None,
@@ -506,14 +199,14 @@ def make_Dipole(
     orientation = orientation * mag_orient
     mag = np.array((0, 0, 1))
     return _update_mag_mesh(
-        dipole, name, name_suffix, mag, orientation, pos, style, **kwargs,
+        dipole, name, name_suffix, mag, orientation, position, style, **kwargs,
     )
 
 
 def make_Cuboid(
     mag=(0.0, 0.0, 1000.0),
-    dim=(1.0, 1.0, 1.0),
-    pos=(0.0, 0.0, 0.0),
+    dimension=(1.0, 1.0, 1.0),
+    position=(0.0, 0.0, 0.0),
     orientation=None,
     style=None,
     **kwargs,
@@ -522,12 +215,12 @@ def make_Cuboid(
     Creates the plotly mesh3d parameters for a Cuboid Magnet in a dictionary based on the
     provided arguments
     """
-    d = [unit_prefix(d / 1000) for d in dim]
+    d = [unit_prefix(d / 1000) for d in dimension]
     default_suffix = f" ({d[0]}m|{d[1]}m|{d[2]}m)"
     name, name_suffix = get_name_and_suffix("Cuboid", default_suffix, style)
-    cuboid = make_BaseCuboid(dim=dim, pos=(0.0, 0.0, 0.0))
+    cuboid = make_BaseCuboid(dimension=dimension, position=(0.0, 0.0, 0.0))
     return _update_mag_mesh(
-        cuboid, name, name_suffix, mag, orientation, pos, style, **kwargs,
+        cuboid, name, name_suffix, mag, orientation, position, style, **kwargs,
     )
 
 
@@ -536,7 +229,7 @@ def make_Cylinder(
     base_vertices=50,
     diameter=1.0,
     height=1.0,
-    pos=(0.0, 0.0, 0.0),
+    position=(0.0, 0.0, 0.0),
     orientation=None,
     style=None,
     **kwargs,
@@ -552,17 +245,17 @@ def make_Cylinder(
         base_vertices=base_vertices,
         diameter=diameter,
         height=height,
-        pos=(0.0, 0.0, 0.0),
+        position=(0.0, 0.0, 0.0),
     )
     return _update_mag_mesh(
-        cylinder, name, name_suffix, mag, orientation, pos, style, **kwargs,
+        cylinder, name, name_suffix, mag, orientation, position, style, **kwargs,
     )
 
 
 def make_CylinderSegment(
     mag=(0.0, 0.0, 1000.0),
     dimension=(1.0, 2.0, 1.0, 0.0, 90.0),
-    pos=(0.0, 0.0, 0.0),
+    position=(0.0, 0.0, 0.0),
     orientation=None,
     Nvert=25.0,
     style=None,
@@ -577,7 +270,14 @@ def make_CylinderSegment(
     name, name_suffix = get_name_and_suffix("CylinderSegment", default_suffix, style)
     cylinder_segment = make_BaseCylinderSegment(*dimension, Nvert=Nvert)
     return _update_mag_mesh(
-        cylinder_segment, name, name_suffix, mag, orientation, pos, style, **kwargs,
+        cylinder_segment,
+        name,
+        name_suffix,
+        mag,
+        orientation,
+        position,
+        style,
+        **kwargs,
     )
 
 
@@ -585,7 +285,7 @@ def make_Sphere(
     mag=(0.0, 0.0, 1000.0),
     Nvert=15,
     diameter=1,
-    pos=(0.0, 0.0, 0.0),
+    position=(0.0, 0.0, 0.0),
     orientation=None,
     style=None,
     **kwargs,
@@ -596,9 +296,11 @@ def make_Sphere(
     """
     default_suffix = f" (D={unit_prefix(diameter / 1000)}m)"
     name, name_suffix = get_name_and_suffix("Sphere", default_suffix, style)
-    sphere = make_Ellipsoid(Nvert=Nvert, dim=[diameter] * 3, pos=(0.0, 0.0, 0.0))
+    sphere = make_BaseEllipsoid(
+        Nvert=Nvert, dimension=[diameter] * 3, position=(0.0, 0.0, 0.0)
+    )
     return _update_mag_mesh(
-        sphere, name, name_suffix, mag, orientation, pos, style, **kwargs,
+        sphere, name, name_suffix, mag, orientation, position, style, **kwargs,
     )
 
 
@@ -607,14 +309,14 @@ def make_Pixels(positions, size=1) -> dict:
     Creates the plotly mesh3d parameters for Sensor pixels based on pixel positions and chosen size
     For now, only "cube" shape is provided.
     """
-    pixels = [make_BaseCuboid(pos=p, dim=[size] * 3) for p in positions]
+    pixels = [make_BaseCuboid(position=p, dimension=[size] * 3) for p in positions]
     return merge_mesh3d(*pixels)
 
 
 def make_Sensor(
     pixel=(0.0, 0.0, 0.0),
-    dim=(1.0, 1.0, 1.0),
-    pos=(0.0, 0.0, 0.0),
+    dimension=(1.0, 1.0, 1.0),
+    position=(0.0, 0.0, 0.0),
     orientation=None,
     color=None,
     style=None,
@@ -641,7 +343,10 @@ def make_Sensor(
     vertices = np.array([sensor[k] for k in "xyz"]).T
     if color is not None:
         sensor["facecolor"][sensor["facecolor"] == "rgb(238,238,238)"] = color
-    dim = np.array([dim] * 3 if isinstance(dim, (float, int)) else dim[:3], dtype=float)
+    dim = np.array(
+        [dimension] * 3 if isinstance(dimension, (float, int)) else dimension[:3],
+        dtype=float,
+    )
     if autosize is not None:
         dim *= autosize
     if np.squeeze(pixel).ndim == 1:
@@ -670,12 +375,12 @@ def make_Sensor(
             meshes_to_merge.append(pixels_mesh)
         hull_pos = 0.5 * (pixel.max(axis=0) + pixel.min(axis=0))
         hull_dim[hull_dim == 0] = pixel_dim / 2
-        hull_mesh = make_BaseCuboid(pos=hull_pos, dim=hull_dim)
+        hull_mesh = make_BaseCuboid(position=hull_pos, dimension=hull_dim)
         hull_mesh["facecolor"] = np.repeat(color, len(hull_mesh["i"]))
         meshes_to_merge.append(hull_mesh)
     sensor = merge_mesh3d(*meshes_to_merge)
     return _update_mag_mesh(
-        sensor, name, name_suffix, orientation=orientation, position=pos, **kwargs
+        sensor, name, name_suffix, orientation=orientation, position=position, **kwargs
     )
 
 
@@ -702,13 +407,13 @@ def _update_mag_mesh(
                 color_middle = kwargs.get("color", None)
             elif color.mode == "bicolor":
                 color_middle = False
-            mesh_dict["colorscale"] = _getColorscale(
+            mesh_dict["colorscale"] = getColorscale(
                 color_transition=color.transition,
                 color_north=color.north,
                 color_middle=color_middle,
                 color_south=color.south,
             )
-            mesh_dict["intensity"] = _getIntensity(
+            mesh_dict["intensity"] = getIntensity(
                 vertices=vertices, axis=magnetization,
             )
     mesh_dict = place_and_orient_model3d(
@@ -717,63 +422,6 @@ def _update_mag_mesh(
     return {**mesh_dict, **kwargs}
 
 
-def merge_mesh3d(*traces):
-    """
-    Merges a list of plotly mesh3d dictionaries. The `i,j,k` index parameters need to cummulate the
-    indices of each object in order to point to the right vertices in the concatenated vertices.
-    `x,y,z,i,j,k` are mandatory fields, the `intensity` and `facecolor` parameters also get
-    concatenated if they are present in all objects. All other parameter found in the dictionary
-    keys are taken from the first object, other keys from further objects are ignored.
-    """
-    merged_trace = {}
-    L = np.array([0] + [len(b["x"]) for b in traces[:-1]]).cumsum()
-    for k in "ijk":
-        if k in traces[0]:
-            merged_trace[k] = np.hstack([b[k] + l for b, l in zip(traces, L)])
-    for k in "xyz":
-        merged_trace[k] = np.concatenate([b[k] for b in traces])
-    for k in ("intensity", "facecolor"):
-        if k in traces[0] and traces[0][k] is not None:
-            merged_trace[k] = np.hstack([b[k] for b in traces])
-    for k, v in traces[0].items():
-        if k not in merged_trace:
-            merged_trace[k] = v
-    return merged_trace
-
-
-def merge_scatter3d(*traces):
-    """
-    Merges a list of plotly scatter3d. `x,y,z` are mandatory fields and are concatenated with a
-    `None` vertex to prevent line connection between objects to be concatenated. Keys are taken from
-    the first object, other keys from further objects are ignored.
-    """
-    merged_trace = {}
-    for k in "xyz":
-        merged_trace[k] = np.hstack([pts for b in traces for pts in [[None], b[k]]])
-    for k, v in traces[0].items():
-        if k not in merged_trace:
-            merged_trace[k] = v
-    return merged_trace
-
-
-def merge_traces(*traces):
-    """
-    Merges a list of plotly 3d-traces. Supported trace types are `mesh3d` and `scatter3d`.
-    All traces have be of the same type when merging. Keys are taken from the first object, other
-    keys from further objects are ignored.
-    """
-    if len(traces) > 1:
-        if traces[0]["type"] == "mesh3d":
-            trace = merge_mesh3d(*traces)
-        elif traces[0]["type"] == "scatter3d":
-            trace = merge_scatter3d(*traces)
-    elif len(traces) == 1:
-        trace = traces[0]
-    else:
-        trace = []
-    return trace
-
-
 def get_name_and_suffix(default_name, default_suffix, style):
     """provides legend entry based on name and suffix"""
     name = default_name if style.name is None else style.name
@@ -792,6 +440,9 @@ def get_plotly_traces(
     path_numbering=False,
     color=None,
     autosize=None,
+    legendgroup=None,
+    showlegend=None,
+    legendtext=None,
     **kwargs,
 ) -> list:
     """
@@ -827,6 +478,7 @@ def get_plotly_traces(
     style_color = getattr(style, "color", None)
     kwargs["color"] = style_color if style_color is not None else color
     kwargs["opacity"] = style.opacity
+    legendgroup = f"{input_obj}" if legendgroup is None else legendgroup
 
     if hasattr(style, "magnetization"):
         if style.magnetization.show:
@@ -859,14 +511,14 @@ def get_plotly_traces(
     else:
         if isinstance(input_obj, Sensor):
             kwargs.update(
-                dim=getattr(input_obj, "dimension", style.size),
+                dimension=getattr(input_obj, "dimension", style.size),
                 pixel=getattr(input_obj, "pixel", (0.0, 0.0, 0.0)),
                 autosize=autosize,
             )
             make_func = make_Sensor
         elif isinstance(input_obj, Cuboid):
             kwargs.update(
-                mag=input_obj.magnetization, dim=input_obj.dimension,
+                mag=input_obj.magnetization, dimension=input_obj.dimension,
             )
             make_func = make_Cuboid
         elif isinstance(input_obj, Cylinder):
@@ -908,9 +560,11 @@ def get_plotly_traces(
                 diameter=input_obj.diameter, current=input_obj.current,
             )
             make_func = make_Loop
+        elif getattr(input_obj, "_object_type", None) == "Collection":
+            make_func = None
         else:
-            kwargs.update(name=type(input_obj).__name__)
-            make_func = make_UnsupportedObject
+            kwargs.update(obj=input_obj)
+            make_func = make_DefaultTrace
 
         path_traces = []
         path_traces_extra = {}
@@ -921,8 +575,10 @@ def get_plotly_traces(
             t for t in extra_model3d_traces if t.backend == "plotly"
         ]
         for orient, pos in zip(*get_rot_pos_from_path(input_obj, show_path)):
-            if style.model3d.show:
-                path_traces.append(make_func(pos=pos, orientation=orient, **kwargs))
+            if style.model3d.show and make_func is not None:
+                path_traces.append(
+                    make_func(position=pos, orientation=orient, **kwargs)
+                )
             for extr in extra_model3d_traces:
                 if extr.show:
                     trace3d = {}
@@ -955,15 +611,22 @@ def get_plotly_traces(
             )
             extra_model3d_trace.update(
                 {
-                    "legendgroup": f"{input_obj}",
-                    "showlegend": not style.model3d.show and ind == 0,
+                    "legendgroup": legendgroup,
+                    "showlegend": showlegend and ind == 0 and not trace,
                     "name": name,
                 }
             )
             traces.append(extra_model3d_trace)
 
         if trace:
-            trace.update({"legendgroup": f"{input_obj}", "showlegend": True})
+            trace.update(
+                {
+                    "legendgroup": legendgroup,
+                    "showlegend": True if showlegend is None else showlegend,
+                }
+            )
+            if legendtext is not None:
+                trace["name"] = legendtext
             traces.append(trace)
 
         if (
@@ -971,184 +634,50 @@ def get_plotly_traces(
             and show_path is not False
             and style.path.show is not False
         ):
-            x, y, z = input_obj.position.T
-            txt_kwargs = (
-                {"mode": "markers+text+lines", "text": list(range(len(x)))}
-                if path_numbering
-                else {"mode": "markers+lines"}
-            )
-            marker = style.path.marker.as_dict()
-            symb = marker["symbol"]
-            marker["symbol"] = SYMBOLS_MATPLOTLIB_TO_PLOTLY.get(symb, symb)
-            marker["color"] = (
-                kwargs["color"] if marker["color"] is None else marker["color"]
-            )
-            marker["size"] *= SIZE_FACTORS_MATPLOTLIB_TO_PLOTLY["marker_size"]
-            line = style.path.line.as_dict()
-            dash = line["style"]
-            line["dash"] = LINESTYLES_MATPLOTLIB_TO_PLOTLY.get(dash, dash)
-            line["color"] = kwargs["color"] if line["color"] is None else line["color"]
-            line["width"] *= SIZE_FACTORS_MATPLOTLIB_TO_PLOTLY["line_width"]
-            line = {k: v for k, v in line.items() if k != "style"}
-            scatter_path = dict(
-                type="scatter3d",
-                x=x,
-                y=y,
-                z=z,
-                name=f"Path: {input_obj}",
-                showlegend=False,
-                legendgroup=f"{input_obj}",
-                marker=marker,
-                line=line,
-                **txt_kwargs,
-                opacity=kwargs["opacity"],
+            scatter_path = make_path(
+                input_obj, path_numbering, style, legendgroup, kwargs
             )
             traces.append(scatter_path)
 
     return traces
 
 
-def display_plotly(
-    *obj_list,
-    markers=None,
-    show_path=True,
-    zoom=1,
-    fig=None,
-    renderer=None,
-    animate_time=5,
-    animate_fps=30,
-    animate_slider=None,
-    color_sequence=None,
-    **kwargs,
-):
-
-    """
-    Display objects and paths graphically using the plotly library.
-
-    Parameters
-    ----------
-    objects: sources, collections or sensors
-        Objects to be displayed.
-
-    markers: array_like, None, shape (N,3), default=None
-        Display position markers in the global CS. By default no marker is displayed.
-
-    show_path: bool or int or array_like, default=True
-        Options True, False, positive int or iterable. By default object paths are shown. If
-        show_path is a positive integer, objects will be displayed at multiple path
-        positions along the path, in steps of show_path. If show_path is an iterable
-        of integers, objects will be displayed for the provided indices.
-        If show_path='animate, the plot will be animated according to the `animate` parameters.
-
-    zoom: float, default = 1
-        Adjust plot zoom-level. When zoom=0 all objects are just inside the 3D-axes.
-
-    fig: plotly Figure, default=None
-        Display graphical output in a given figure:
-        - plotly.graph_objects.Figure
-        - plotly.graph_objects.FigureWidget
-        By default a new `Figure` is created and displayed.
-
-    renderer: str. default=None,
-        The renderers framework is a flexible approach for displaying plotly.py figures in a variety
-        of contexts.
-        Available renderers are:
-        ['plotly_mimetype', 'jupyterlab', 'nteract', 'vscode',
-         'notebook', 'notebook_connected', 'kaggle', 'azure', 'colab',
-         'cocalc', 'databricks', 'json', 'png', 'jpeg', 'jpg', 'svg',
-         'pdf', 'browser', 'firefox', 'chrome', 'chromium', 'iframe',
-         'iframe_connected', 'sphinx_gallery', 'sphinx_gallery_png']
-
-    animate_time: float, default = 3
-        Sets the animation duration
-
-    animate_fps: float, default = 30
-        This sets the maximum allowed frame rate. In case of path positions needed to be displayed
-        exceeds the `animate_fps` the path position will be downsampled to be lower or equal
-        the `animate_fps`. This is mainly depending on the pc/browser performance and is set to
-        50 by default to avoid hanging the animation process.
-
-    animate_slider: bool, default = False
-        if True, an interactive slider will be displayed and stay in sync with the animation
-
-    title: str, default = "3D-Paths Animation"
-        When zoom=0 all objects are just inside the 3D-axes.
-
-    color_sequence: list or array_like, iterable, default=
-            ['#2E91E5', '#E15F99', '#1CA71C', '#FB0D0D', '#DA16FF', '#222A2A',
-            '#B68100', '#750D86', '#EB663B', '#511CFB', '#00A08B', '#FB00D1',
-            '#FC0080', '#B2828D', '#6C7C32', '#778AAE', '#862A16', '#A777F1',
-            '#620042', '#1616A7', '#DA60CA', '#6C4516', '#0D2A63', '#AF0038']
-        An iterable of color values used to cycle trough for every object displayed.
-        A color and may be specified as:
-      - A hex string (e.g. '#ff0000')
-      - An rgb/rgba string (e.g. 'rgb(255,0,0)')
-      - An hsl/hsla string (e.g. 'hsl(0,100%,50%)')
-      - An hsv/hsva string (e.g. 'hsv(0,100%,100%)')
-      - A named CSS color
-
-    Returns
-    -------
-    None: NoneType
-    """
-
-    show_fig = False
-    if fig is None:
-        show_fig = True
-        fig = go.Figure()
-    if animate_slider is None:
-        animate_slider = Config.display.animation.slider
-
-    if obj_list:
-        style = getattr(obj_list[0], "style", None)
-        name = getattr(style, "name", None)
-        title = name if len(obj_list) == 1 else None
-    else:
-        title = "No objects to be displayed"
-
-    if markers is not None and markers:
-        obj_list = list(obj_list) + [MagpyMarkers(*markers)]
-
-    if color_sequence is None:
-        color_sequence = Config.display.colorsequence
-
-    with fig.batch_update():
-        if (
-            not any(getattr(obj, "position", np.array([])).ndim > 1 for obj in obj_list)
-            and show_path == "animate"
-        ):  # check if some path exist for any object
-            show_path = True
-            warnings.warn("No path to be animated detected, displaying standard plot")
-
-        if show_path == "animate":
-            title = "3D-Paths Animation" if title is None else title
-            animate_path(
-                fig=fig,
-                objs=obj_list,
-                color_sequence=color_sequence,
-                zoom=zoom,
-                title=title,
-                animate_time=animate_time,
-                animate_fps=animate_fps,
-                animate_slider=animate_slider,
-                **kwargs,
-            )
-        else:
-            traces_dicts = draw_frame(
-                obj_list, color_sequence, zoom, show_path, **kwargs
-            )
-            traces = [t for obj in obj_list for t in traces_dicts[obj]]
-            fig.add_traces(traces)
-            fig.update_layout(title_text=title)
-            apply_fig_ranges(fig, zoom=zoom)
-        fig.update_layout(legend_itemsizing="constant")
-    if show_fig:
-        fig.show(renderer=renderer)
+def make_path(input_obj, path_numbering, style, legendgroup, kwargs):
+    """draw obj path based on path style properties"""
+    x, y, z = input_obj.position.T
+    txt_kwargs = (
+        {"mode": "markers+text+lines", "text": list(range(len(x)))}
+        if path_numbering
+        else {"mode": "markers+lines"}
+    )
+    marker = style.path.marker.as_dict()
+    symb = marker["symbol"]
+    marker["symbol"] = SYMBOLS_MATPLOTLIB_TO_PLOTLY.get(symb, symb)
+    marker["color"] = kwargs["color"] if marker["color"] is None else marker["color"]
+    marker["size"] *= SIZE_FACTORS_MATPLOTLIB_TO_PLOTLY["marker_size"]
+    line = style.path.line.as_dict()
+    dash = line["style"]
+    line["dash"] = LINESTYLES_MATPLOTLIB_TO_PLOTLY.get(dash, dash)
+    line["color"] = kwargs["color"] if line["color"] is None else line["color"]
+    line["width"] *= SIZE_FACTORS_MATPLOTLIB_TO_PLOTLY["line_width"]
+    line = {k: v for k, v in line.items() if k != "style"}
+    scatter_path = dict(
+        type="scatter3d",
+        x=x,
+        y=y,
+        z=z,
+        name=f"Path: {input_obj}",
+        showlegend=False,
+        legendgroup=legendgroup,
+        marker=marker,
+        line=line,
+        **txt_kwargs,
+        opacity=kwargs["opacity"],
+    )
+    return scatter_path
 
 
-def draw_frame(
-    objs, color_sequence, zoom, show_path, return_autosize=False, **kwargs
-) -> Tuple:
+def draw_frame(objs, color_sequence, zoom, show_path, autosize=None, **kwargs) -> Tuple:
     """
     Creates traces from input `objs` and provided parameters, updates the size of objects like
     Sensors and Dipoles in `kwargs` depending on the canvas size.
@@ -1158,23 +687,59 @@ def draw_frame(
     traces_dicts, kwargs: dict, dict
         returns the traces in a obj/traces_list dictionary and updated kwargs
     """
+    return_autosize = False
     Sensor = _src.obj_classes.Sensor
     Dipole = _src.obj_classes.Dipole
     traces_dicts = {}
     traces_colors = {}
     for obj, color in zip(objs, cycle(color_sequence)):
-        if not isinstance(obj, (Dipole, Sensor)):
-            traces_dicts[obj] = get_plotly_traces(
-                obj, show_path=show_path, color=color, **kwargs
-            )
-        else:
+        if isinstance(obj, (Dipole, Sensor)):
             traces_colors[obj] = color
             # temporary coordinates to be able to calculate ranges
             x, y, z = obj.position.T
             traces_dicts[obj] = [dict(x=x, y=y, z=z)]
+        subobjs = [obj]
+        legendgroup = None
+        if getattr(obj, "children", None) is not None:
+            subobjs = obj.children
+            legendgroup = f"{obj}"
+            if getattr(obj, "position", None) is not None:
+                subobjs += [obj]
+                color = color if obj.style.color is None else obj.style.color
+        first_shown = False
+        for ind, subobj in enumerate(subobjs):
+            if legendgroup is not None:
+                if (subobj.style.model3d.show and not first_shown) or ind == len(
+                    subobjs
+                ):  # take name of parent
+                    first_shown = True
+                    showlegend = True
+                    legendtext = getattr(getattr(obj, "style", None), "name", None)
+                    legendtext = (
+                        getattr(obj, "name", None) if legendtext is None else legendtext
+                    )
+                    legendtext = f"{obj!r}" if legendtext is None else legendtext
+                else:
+                    legendtext = None
+                    showlegend = False
+            else:
+                showlegend = True
+                legendtext = None
+            traces_dicts[subobj] = get_plotly_traces(
+                subobj,
+                show_path=show_path,
+                color=color,
+                legendgroup=legendgroup,
+                showlegend=showlegend,
+                legendtext=legendtext,
+                **kwargs,
+            )
     traces = [t for tr in traces_dicts.values() for t in tr]
     ranges = get_scene_ranges(*traces, zoom=zoom)
-    autosize = np.mean(np.diff(ranges)) / Config.display.autosizefactor
+    if autosize is None or autosize == "return":
+        if autosize == "return":
+            return_autosize = True
+        autosize = np.mean(np.diff(ranges)) / Config.display.autosizefactor
     for obj, color in traces_colors.items():
         traces_dicts[obj] = get_plotly_traces(
             obj, show_path=show_path, color=color, autosize=autosize, **kwargs
@@ -1193,7 +758,7 @@ def apply_fig_ranges(fig, ranges=None, zoom=None):
 
     Parameters
     ----------
-    ranges: array of dim=(3,2)
+    ranges: array of dimension=(3,2)
         min and max graph range
 
     zoom: float, default = 1
@@ -1295,12 +860,15 @@ def animate_path(
     """
     # make sure the number of frames does not exceed the max frames and max frame rate
     # downsample if necessary
-    path_lengths = [
-        getattr(obj, "position", np.array((0.0, 0.0, 0.0))).shape[0]
-        if getattr(obj, "position", np.array((0.0, 0.0, 0.0))).ndim > 1
-        else 0
-        for obj in objs
-    ]
+    path_lengths = []
+    for obj in objs:
+        subobjs = [obj]
+        if getattr(obj, "_object_type", None) == "Collection":
+            subobjs.extend(obj.children)
+        for subobj in subobjs:
+            path_len = getattr(subobj, "_position", np.array((0.0, 0.0, 0.0))).shape[0]
+            path_lengths.append(path_len)
+
     max_pl = max(path_lengths)
     maxfps = Config.display.animation.maxfps
     maxframes = Config.display.animation.maxframes
@@ -1395,23 +963,15 @@ def animate_path(
 
     # create frame for each path index or downsampled path index
     frames = []
+    autosize = "return"
     for i, ind in enumerate(path_indices):
-        if i == 0:  # calculate the dipoles and sensors autosize from first frame
-            traces_dicts, autosize = draw_frame(
-                objs,
-                color_sequence,
-                zoom,
-                show_path=[ind],
-                return_autosize=True,
-                **kwargs,
-            )
+        frame = draw_frame(
+            objs, color_sequence, zoom, show_path=[ind], autosize=autosize, **kwargs,
+        )
+        if i == 0:  # get the dipoles and sensors autosize from first frame
+            traces_dicts, autosize = frame
         else:
-            traces_dicts = {
-                obj: get_plotly_traces(
-                    obj, show_path=[ind], color=color, autosize=autosize, **kwargs
-                )
-                for obj, color in zip(objs, cycle(color_sequence))
-            }
+            traces_dicts = frame
         traces = [t for tr in traces_dicts.values() for t in tr]
         frames.append(
             go.Frame(
@@ -1441,3 +1001,144 @@ def animate_path(
         sliders=[sliders_dict] if animate_slider else None,
     )
     apply_fig_ranges(fig, zoom=zoom)
+
+
+def display_plotly(
+    *obj_list,
+    markers=None,
+    show_path=True,
+    zoom=1,
+    fig=None,
+    renderer=None,
+    animate_time=5,
+    animate_fps=30,
+    animate_slider=None,
+    color_sequence=None,
+    **kwargs,
+):
+
+    """
+    Display objects and paths graphically using the plotly library.
+
+    Parameters
+    ----------
+    objects: sources, collections or sensors
+        Objects to be displayed.
+
+    markers: array_like, None, shape (N,3), default=None
+        Display position markers in the global CS. By default no marker is displayed.
+
+    show_path: bool or int or array_like, default=True
+        Options True, False, positive int or iterable. By default object paths are shown. If
+        show_path is a positive integer, objects will be displayed at multiple path
+        positions along the path, in steps of show_path. If show_path is an iterable
+        of integers, objects will be displayed for the provided indices.
+        If show_path='animate, the plot will be animated according to the `animate` parameters.
+
+    zoom: float, default = 1
+        Adjust plot zoom-level. When zoom=0 all objects are just inside the 3D-axes.
+
+    fig: plotly Figure, default=None
+        Display graphical output in a given figure:
+        - plotly.graph_objects.Figure
+        - plotly.graph_objects.FigureWidget
+        By default a new `Figure` is created and displayed.
+
+    renderer: str. default=None,
+        The renderers framework is a flexible approach for displaying plotly.py figures in a variety
+        of contexts.
+        Available renderers are:
+        ['plotly_mimetype', 'jupyterlab', 'nteract', 'vscode',
+         'notebook', 'notebook_connected', 'kaggle', 'azure', 'colab',
+         'cocalc', 'databricks', 'json', 'png', 'jpeg', 'jpg', 'svg',
+         'pdf', 'browser', 'firefox', 'chrome', 'chromium', 'iframe',
+         'iframe_connected', 'sphinx_gallery', 'sphinx_gallery_png']
+
+    animate_time: float, default = 3
+        Sets the animation duration
+
+    animate_fps: float, default = 30
+        This sets the maximum allowed frame rate. In case of path positions needed to be displayed
+        exceeds the `animate_fps` the path position will be downsampled to be lower or equal
+        the `animate_fps`. This is mainly depending on the pc/browser performance and is set to
+        50 by default to avoid hanging the animation process.
+
+    animate_slider: bool, default = False
+        if True, an interactive slider will be displayed and stay in sync with the animation
+
+    title: str, default = "3D-Paths Animation"
+        When zoom=0 all objects are just inside the 3D-axes.
+
+    color_sequence: list or array_like, iterable, default=
+            ['#2E91E5', '#E15F99', '#1CA71C', '#FB0D0D', '#DA16FF', '#222A2A',
+            '#B68100', '#750D86', '#EB663B', '#511CFB', '#00A08B', '#FB00D1',
+            '#FC0080', '#B2828D', '#6C7C32', '#778AAE', '#862A16', '#A777F1',
+            '#620042', '#1616A7', '#DA60CA', '#6C4516', '#0D2A63', '#AF0038']
+        An iterable of color values used to cycle trough for every object displayed.
+        A color and may be specified as:
+      - A hex string (e.g. '#ff0000')
+      - An rgb/rgba string (e.g. 'rgb(255,0,0)')
+      - An hsl/hsla string (e.g. 'hsl(0,100%,50%)')
+      - An hsv/hsva string (e.g. 'hsv(0,100%,100%)')
+      - A named CSS color
+
+    Returns
+    -------
+    None: NoneType
+    """
+
+    show_fig = False
+    if fig is None:
+        show_fig = True
+        fig = go.Figure()
+    if animate_slider is None:
+        animate_slider = Config.display.animation.slider
+
+    if obj_list:
+        style = getattr(obj_list[0], "style", None)
+        name = getattr(style, "name", None)
+        title = name if len(obj_list) == 1 else None
+    else:
+        title = "No objects to be displayed"
+
+    if markers is not None and markers:
+        obj_list = list(obj_list) + [MagpyMarkers(*markers)]
+
+    if color_sequence is None:
+        color_sequence = Config.display.colorsequence
+
+    with fig.batch_update():
+        flat_obj_list = format_obj_input(obj_list)
+        if (
+            not any(
+                getattr(obj, "position", np.array([])).ndim > 1 for obj in flat_obj_list
+            )
+            and show_path == "animate"
+        ):  # check if some path exist for any object
+            show_path = True
+            warnings.warn("No path to be animated detected, displaying standard plot")
+
+        if show_path == "animate":
+            title = "3D-Paths Animation" if title is None else title
+            animate_path(
+                fig=fig,
+                objs=obj_list,
+                color_sequence=color_sequence,
+                zoom=zoom,
+                title=title,
+                animate_time=animate_time,
+                animate_fps=animate_fps,
+                animate_slider=animate_slider,
+                **kwargs,
+            )
+        else:
+            traces_dicts = draw_frame(
+                obj_list, color_sequence, zoom, show_path, **kwargs
+            )
+            traces = [t for traces in traces_dicts.values() for t in traces]
+            fig.add_traces(traces)
+            fig.update_layout(title_text=title)
+            apply_fig_ranges(fig, zoom=zoom)
+        fig.update_layout(legend_itemsizing="constant")
+    if show_fig:
+        fig.show(renderer=renderer)
diff --git a/magpylib/_src/display/sensor_plotly_mesh.py b/magpylib/_src/display/plotly/plotly_sensor_mesh.py
similarity index 100%
rename from magpylib/_src/display/sensor_plotly_mesh.py
rename to magpylib/_src/display/plotly/plotly_sensor_mesh.py
diff --git a/magpylib/_src/display/plotly/plotly_utility.py b/magpylib/_src/display/plotly/plotly_utility.py
new file mode 100644
index 000000000..bfb7e20d0
--- /dev/null
+++ b/magpylib/_src/display/plotly/plotly_utility.py
@@ -0,0 +1,133 @@
+"""utility functions for plotly backend"""
+import numpy as np
+
+def merge_mesh3d(*traces):
+    """
+    Merges a list of plotly mesh3d dictionaries. The `i,j,k` index parameters need to cummulate the
+    indices of each object in order to point to the right vertices in the concatenated vertices.
+    `x,y,z,i,j,k` are mandatory fields, the `intensity` and `facecolor` parameters also get
+    concatenated if they are present in all objects. All other parameter found in the dictionary
+    keys are taken from the first object, other keys from further objects are ignored.
+    """
+    merged_trace = {}
+    L = np.array([0] + [len(b["x"]) for b in traces[:-1]]).cumsum()
+    for k in "ijk":
+        if k in traces[0]:
+            merged_trace[k] = np.hstack([b[k] + l for b, l in zip(traces, L)])
+    for k in "xyz":
+        merged_trace[k] = np.concatenate([b[k] for b in traces])
+    for k in ("intensity", "facecolor"):
+        if k in traces[0] and traces[0][k] is not None:
+            merged_trace[k] = np.hstack([b[k] for b in traces])
+    for k, v in traces[0].items():
+        if k not in merged_trace:
+            merged_trace[k] = v
+    return merged_trace
+
+
+def merge_scatter3d(*traces):
+    """
+    Merges a list of plotly scatter3d. `x,y,z` are mandatory fields and are concatenated with a
+    `None` vertex to prevent line connection between objects to be concatenated. Keys are taken from
+    the first object, other keys from further objects are ignored.
+    """
+    merged_trace = {}
+    for k in "xyz":
+        merged_trace[k] = np.hstack([pts for b in traces for pts in [[None], b[k]]])
+    for k, v in traces[0].items():
+        if k not in merged_trace:
+            merged_trace[k] = v
+    return merged_trace
+
+
+def merge_traces(*traces):
+    """
+    Merges a list of plotly 3d-traces. Supported trace types are `mesh3d` and `scatter3d`.
+    All traces have be of the same type when merging. Keys are taken from the first object, other
+    keys from further objects are ignored.
+    """
+    if len(traces) > 1:
+        if traces[0]["type"] == "mesh3d":
+            trace = merge_mesh3d(*traces)
+        elif traces[0]["type"] == "scatter3d":
+            trace = merge_scatter3d(*traces)
+    elif len(traces) == 1:
+        trace = traces[0]
+    else:
+        trace = []
+    return trace
+
+
+def getIntensity(vertices, axis) -> np.ndarray:
+    """
+    Calculates the intensity values for vertices based on the distance of the vertices to the mean
+    vertices position in the provided axis direction. It can be used for plotting
+    fields on meshes. If `mag` See more infos here:https://plotly.com/python/3d-mesh/
+
+    Parameters
+    ----------
+    vertices : ndarray Nx3
+        the N vertices of the mesh object
+    axis : ndarray 3
+        direction vector
+
+    Returns
+    -------
+    ndarray N
+        returns 1D array of length N
+    """
+    p = np.array(vertices).T
+    pos = np.mean(p, axis=1)
+    m = np.array(axis) / np.linalg.norm(axis)
+    intensity = (p[0] - pos[0]) * m[0] + (p[1] - pos[1]) * m[1] + (p[2] - pos[2]) * m[2]
+    return intensity
+
+
+def getColorscale(
+    color_transition=0,
+    color_north="#E71111",  # 'red'
+    color_middle="#DDDDDD",  # 'grey'
+    color_south="#00B050",  # 'green'
+) -> list:
+    """
+    Provides the colorscale for a plotly mesh3d trace.
+    The colorscale must be an array containing arrays mapping a normalized value to an rgb, rgba,
+    hex, hsl, hsv, or named color string. At minimum, a mapping for the lowest (0) and highest (1)
+    values are required. For example, `[[0, 'rgb(0,0,255)'], [1,'rgb(255,0,0)']]`.
+    In this case the colorscale is created depending on the north/middle/south poles colors. If the
+    middle color is `None` the colorscale will only have north and south pole colors.
+
+    Parameters
+    ----------
+    color_transition : float, optional
+        A value between 0 and 1. Sets the smoothness of the color transitions from adjacent colors
+        visualization., by default 0.1
+    color_north : str, optional
+        magnetic north pole color , by default None
+    color_middle : str, optional
+        middle between south and north pole color, by default None
+    color_south : str, optional
+        magnetic north pole color , by default None
+
+    Returns
+    -------
+    list
+        returns colorscale as list of tuples
+    """
+    if color_middle is False:
+        colorscale = [
+            [0.0, color_south],
+            [0.5 * (1 - color_transition), color_south],
+            [0.5 * (1 + color_transition), color_north],
+            [1, color_north],
+        ]
+    else:
+        colorscale = [
+            [0.0, color_south],
+            [0.2 - 0.2 * (color_transition), color_south],
+            [0.2 + 0.3 * (color_transition), color_middle],
+            [0.8 - 0.3 * (color_transition), color_middle],
+            [0.8 + 0.2 * (color_transition), color_north],
+            [1.0, color_north],
+        ]
+    return colorscale
diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py
index 74fd36fc7..522d54191 100644
--- a/magpylib/_src/fields/field_BH_cuboid.py
+++ b/magpylib/_src/fields/field_BH_cuboid.py
@@ -4,7 +4,7 @@
 """
 
 import numpy as np
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 
 
 def field_BH_cuboid(
diff --git a/magpylib/_src/fields/field_BH_cylinder_tile.py b/magpylib/_src/fields/field_BH_cylinder_tile.py
index 6c765b5a2..3333782d8 100644
--- a/magpylib/_src/fields/field_BH_cylinder_tile.py
+++ b/magpylib/_src/fields/field_BH_cylinder_tile.py
@@ -6,7 +6,7 @@
 from scipy.special import ellipeinc, ellipkinc
 from magpylib._src.fields.special_el3 import el3_angle
 from magpylib._src.utility import close
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 
 
 def arctan_k_tan_2(k, phi):
diff --git a/magpylib/_src/fields/field_BH_line.py b/magpylib/_src/fields/field_BH_line.py
index 99ec8767e..79b8b1c2f 100644
--- a/magpylib/_src/fields/field_BH_line.py
+++ b/magpylib/_src/fields/field_BH_line.py
@@ -4,7 +4,7 @@
 
 import numpy as np
 from numpy.linalg import norm
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 
 
 def field_BH_line_from_vert(
diff --git a/magpylib/_src/fields/field_wrap_BH_level2.py b/magpylib/_src/fields/field_wrap_BH_level2.py
index 969c2f2f5..1a29e31f1 100644
--- a/magpylib/_src/fields/field_wrap_BH_level2.py
+++ b/magpylib/_src/fields/field_wrap_BH_level2.py
@@ -229,11 +229,11 @@ def getBH_level2(bh, sources, observers, sumup, squeeze) -> np.ndarray:
             m_tile = m-m0
             # tile up position
             tile_pos = np.tile(obj._position[-1], (m_tile,1))
-            obj.position = np.concatenate((obj._position, tile_pos))
+            obj._position = np.concatenate((obj._position, tile_pos))
             # tile up orientation
             tile_orient = np.tile(obj._orientation.as_quat()[-1], (m_tile,1))
             tile_orient = np.concatenate((obj._orientation.as_quat(), tile_orient))
-            obj.orientation = R.from_quat(tile_orient)
+            obj._orientation = R.from_quat(tile_orient)
 
     # combine information form all sensors to generate pos_obs with-------------
     #   shape (m * concat all sens flat pixel, 3)
@@ -312,7 +312,7 @@ def getBH_level2(bh, sources, observers, sumup, squeeze) -> np.ndarray:
 
     # reset tiled objects
     for obj,m0 in zip(reset_obj, reset_obj_m0):
-        obj.position = obj.position[:m0]
-        obj.orientation = obj.orientation[:m0]
+        obj._position = obj._position[:m0]
+        obj._orientation = obj._orientation[:m0]
 
     return B
diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py
index fd0c72310..c30d44a05 100644
--- a/magpylib/_src/input_checks.py
+++ b/magpylib/_src/input_checks.py
@@ -58,32 +58,26 @@ def check_anchor_type(anch):
 
 
 def check_anchor_format(anch):
-    """must be shape (3,) or 0"""
-    if anch is not None:
-        if not np.all(anch == np.array(0)):
-            if not anch.shape == (3,):
-                msg = "Bad anchor input. Must be None, 0 or shape (3,)."
-                raise MagpylibBadInputShape(msg)
+    """must be shape (N,3), (3,) or 0"""
+    if not np.all(anch == np.array(0)):
+        msg = "Bad anchor input. Must be None, 0 or shape (3,) or (N,3)."
+        if not anch.shape[-1] == 3:
+            raise MagpylibBadInputShape(msg)
+        if not anch.ndim in (1, 2):
+            raise MagpylibBadInputShape(msg)
 
 
 def check_rot_type(inp):
     """rotation input mut be scipy Rotation"""
     if not isinstance(inp, (Rotation, type(None))):
-        msg = "rot input must be None or scipy Rotation object."
+        msg = "orientation input must be None or scipy Rotation object."
         raise MagpylibBadUserInput(msg)
 
 
 def check_start_type(start):
     """start input must be int or str"""
-    if not (isinstance(start, int) or start == "append"):
-        msg = 'start input must be int or str ("attach")'
-        raise MagpylibBadUserInput(msg)
-
-
-def check_increment_type(inrc):
-    """incremnt input must be bool"""
-    if not isinstance(inrc, bool):
-        msg = "increment input must be bool (True or False)."
+    if not (isinstance(start, int) or start == 'auto'):
+        msg = 'start input must be int or str ("auto")'
         raise MagpylibBadUserInput(msg)
 
 
@@ -97,10 +91,11 @@ def check_angle_type(angle):
 
 
 def check_angle_format(angle):
-    """angle format must be of shape (N,)"""
-    if not angle.ndim == 1:
-        msg = "Bad angle input shape. Must be scalar or 1D vector."
-        raise MagpylibBadInputShape(msg)
+    """angle format must be scalar or of shape (N,)"""
+    if isinstance(angle, np.ndarray):
+        if angle.ndim not in (0,1):
+            msg = "Bad angle input shape. Must be scalar or 1D vector."
+            raise MagpylibBadInputShape(msg)
 
 
 def check_axis_type(ax):
diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py
index a39a8c0aa..6f710d03a 100644
--- a/magpylib/_src/obj_classes/class_BaseExcitations.py
+++ b/magpylib/_src/obj_classes/class_BaseExcitations.py
@@ -1,7 +1,7 @@
 """BaseHomMag class code"""
 
 import numpy as np
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import (
     check_vector_type,
     check_vector_format,
diff --git a/magpylib/_src/obj_classes/class_BaseGeo.py b/magpylib/_src/obj_classes/class_BaseGeo.py
index e91016318..b930c7254 100644
--- a/magpylib/_src/obj_classes/class_BaseGeo.py
+++ b/magpylib/_src/obj_classes/class_BaseGeo.py
@@ -1,32 +1,77 @@
 """BaseGeo class code"""
 
+# pylint: disable=cyclic-import
+# pylint: disable=too-many-instance-attributes
+# pylint: disable=protected-access
+
 import numpy as np
 from scipy.spatial.transform import Rotation as R
-from magpylib._src.obj_classes.class_Collection import Collection
-from magpylib._src.obj_classes.class_BaseRotation import BaseRotation
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.obj_classes.class_BaseTransform import BaseTransform
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import (
     check_vector_type,
     check_path_format,
-    check_start_type,
-    check_increment_type,
-    check_rot_type,
-)
-from magpylib._src.utility import adjust_start
-
-# ALL METHODS ON INTERFACE
-class BaseGeo(BaseRotation):
-    """Initializes position and rotation (=orientation) properties
+    check_rot_type)
+
+
+def pad_slice_path(path1, path2):
+    """
+    edge-pads or end-slices path 2 to fit path 1 format
+    path1: shape (N,x)
+    path2: shape (M,x)
+    return: path2 with format (N,x)
+    """
+    delta_path = len(path1) - len(path2)
+    if delta_path>0:
+        return np.pad(path2, ((0,delta_path), (0,0)), 'edge')
+    if delta_path<0:
+        return path2[-delta_path:]
+    return path2
+
+
+def position_input_check(pos):
+    """
+    checks input type and format end returns an ndarray of shape (N,3).
+    This function is used for setter and init only -> (1,3) and (3,) input
+    creates same behavior.
+    """
+    # check input type
+    if Config.checkinputs:
+        check_vector_type(pos, "position")
+    # path vector -> ndarray
+    pos_array = np.array(pos, dtype=float)
+    # check input format
+    if Config.checkinputs:
+        check_path_format(pos_array, "position")
+    # tile to format (N,3) and return
+    return pos_array.reshape(-1,3)
+
+def orientation_input_check(ori):
+    """
+    checks input type and format end returns an ndarray of shape (N,4).
+    This function is used for setter and init only -> (1,4) and (4,) input
+    creates same behavior.
+    """
+    # check input type
+    if Config.checkinputs:
+        check_rot_type(ori)
+    # None input generates unit rotation
+    ori_array = np.array([(0, 0, 0, 1)]) if ori is None else ori.as_quat()
+    # tile to format (N,4) and return
+    return ori_array.reshape(-1,4)
+
+class BaseGeo(BaseTransform):
+    """Initializes position and orientation properties
     of an object in a global CS.
 
-    Position is a ndarray with shape (3,).
+    position is a ndarray with shape (3,).
 
-    Rotation is a scipy.spatial.transformation.Rotation
+    orientation is a scipy.spatial.transformation.Rotation
     object that gives the relative rotation to the init_state. The
     init_state is defined by how the fields are implemented (e.g.
     cyl upright in xy-plane)
 
-    Both attributes _pos and _rot.as_rotvec() are of shape (N,3),
+    Both attributes _position and _orientation.as_rotvec() are of shape (N,3),
     and describe a path of length N. (N=1 if there is only one
     object position).
 
@@ -47,12 +92,12 @@ class BaseGeo(BaseRotation):
 
     """
 
-    def __init__(self, position, orientation, style=None, **kwargs):
-        # set pos and orient attributes
-        self.position = position
-        self.orientation = orientation
-        super().__init__()
+    def __init__(self, position=(0.,0.,0.,), orientation=None, style=None, **kwargs):
+
+        # set _position and _orientation attributes
+        self._init_position_orientation(position, orientation)
 
+        # style
         self.style_class = self._get_style_class()
         if style is not None or kwargs:
             if style is None:
@@ -68,6 +113,31 @@ def __init__(self, position, orientation, style=None, **kwargs):
             style.update(**style_kwargs)
             self.style = style
 
+    def _init_position_orientation(self, position, orientation):
+        """
+        tile up position and orientation input at Class init and set attributes
+        _position and _orientation.
+        pos: position input
+        ori: orientation.as_quat() input
+        """
+
+        # format position and orientation inputs
+        pos = position_input_check(position)
+        ori = orientation_input_check(orientation)
+
+        # padding logic: if one is longer than the other, edge-pad up the other
+        len_pos = pos.shape[0]
+        len_ori = ori.shape[0]
+
+        if len_pos>len_ori:
+            ori = np.pad(ori, ((0,len_pos-len_ori), (0,0)), 'edge')
+        elif len_pos<len_ori:
+            pos = np.pad(pos, ((0,len_ori-len_pos), (0,0)), 'edge')
+
+        # set attributes
+        self._position = pos
+        self._orientation = R.from_quat(ori)
+
     def _get_style_class(self):
         """returns style class based on object type. If class has no attribute `_object_type` or is
         not found in `MAGPYLIB_FAMILIES` returns `BaseStyle` class."""
@@ -83,28 +153,35 @@ def position(self):
         return np.squeeze(self._position)
 
     @position.setter
-    def position(self, pos):
-        """Set object position-path.
+    def position(self, inp):
+        """
+        Set object position-path.
+
+        Use edge-padding and end-slicing to adjust orientation path
+
+        When a Collection position is set, then all children retain their
+        relative position to the Collection BaseGeo.
 
         position: array_like, shape (3,) or (N,3)
             Position-path of object.
         """
+        old_pos = self._position
 
-        # check input type
-        if Config.checkinputs:
-            check_vector_type(pos, "position")
+        # check and set new position
+        self._position = position_input_check(inp)
 
-        # path vector -> ndarray
-        pos = np.array(pos, dtype=float)
+        # pad/slice and set orientation path to same length
+        oriQ = self._orientation.as_quat()
+        self._orientation = R.from_quat(pad_slice_path(self._position, oriQ))
 
-        # check input format
-        if Config.checkinputs:
-            check_path_format(pos, "position")
+        # when there are children include their relative position
+        for child in getattr(self, "children", []):
+            old_pos = pad_slice_path(self._position, old_pos)
+            child_pos = pad_slice_path(self._position, child._position)
+            rel_child_pos = child_pos - old_pos
+            # set child position (pad/slice orientation)
+            child.position = self._position + rel_child_pos
 
-        # expand if input is shape (3,)
-        if pos.ndim == 1:
-            pos = np.expand_dims(pos, 0)
-        self._position = pos
 
     @property
     def orientation(self):
@@ -115,28 +192,31 @@ def orientation(self):
         return self._orientation  # return full path
 
     @orientation.setter
-    def orientation(self, rot):
+    def orientation(self, inp):
         """Set object orientation-path.
 
-        rot: None or scipy Rotation, shape (1,) or (N,), default=None
+        inp: None or scipy Rotation, shape (1,) or (N,)
             Set orientation-path of object. None generates a unit orientation
             for every path step.
         """
-        # check input type
-        if Config.checkinputs:
-            check_rot_type(rot)
-
-        # None input generates unit rotation
-        if rot is None:
-            self._orientation = R.from_quat([(0, 0, 0, 1)] * len(self._position))
-
-        # expand rot.as_quat() to shape (1,4)
-        else:
-            val = rot.as_quat()
-            if val.ndim == 1:
-                self._orientation = R.from_quat([val])
-            else:
-                self._orientation = rot
+        old_oriQ = self._orientation.as_quat()
+
+        # set _orientation attribute with ndim=2 format
+        oriQ = orientation_input_check(inp)
+        self._orientation = R.from_quat(oriQ)
+
+        # pad/slice position path to same length
+        self._position = pad_slice_path(oriQ, self._position)
+
+        # when there are children they rotate about self.position
+        # after the old Collection orientation is rotated away.
+        for child in getattr(self, "children", []):
+            # pad/slice and set child path
+            child.position = pad_slice_path(self._position, child._position)
+            # compute rotation and apply
+            old_ori_pad = R.from_quat(np.squeeze(pad_slice_path(oriQ, old_oriQ)))
+            child.rotate(self.orientation*old_ori_pad.inv(), anchor=self._position, start=0)
+
 
     @property
     def style(self):
@@ -159,7 +239,7 @@ def _validate_style(self, val=None):
         return val
 
     # dunders -------------------------------------------------------
-    def __add__(self, source):
+    def __add__(self, obj):
         """
         Add up sources to a Collection object.
 
@@ -167,12 +247,14 @@ def __add__(self, source):
         -------
         Collection: Collection
         """
-        return Collection(self, source)
+        # pylint: disable=import-outside-toplevel
+        from magpylib._src.obj_classes.class_Collection import Collection
+        return Collection(self, obj)
 
     # methods -------------------------------------------------------
     def reset_path(self):
         """
-        Reset object path to position = (0,0,0) and orientation = unit rotation.
+        Set object position to (0,0,0) and orientation = unit rotation.
 
         Returns
         -------
@@ -191,134 +273,6 @@ def reset_path(self):
         [0. 0. 0.]
 
         """
-        self.position = (0, 0, 0)
-        self.orientation = R.from_quat((0, 0, 0, 1))
-
-    def move(self, displacement, start=-1, increment=False):
-        """
-        Translates the object by the input displacement (can be a path).
-
-        This method uses vector addition to merge the input path given by displacement and the
-        existing old path of an object. It keeps the old orientation. If the input path extends
-        beyond the old path, the old path will be padded by its last entry before paths are
-        added up.
-
-        Parameters
-        ----------
-        displacement: array_like, shape (3,) or (N,3)
-            Displacement vector shape=(3,) or path shape=(N,3) in units of [mm].
-
-        start: int or str, default=-1
-            Choose at which index of the original object path, the input path will begin.
-            If `start=-1`, inp_path will start at the last old_path position.
-            If `start=0`, inp_path will start with the beginning of the old_path.
-            If `start=len(old_path)` or `start='append'`, inp_path will be attached to
-            the old_path.
-
-        increment: bool, default=False
-            If `increment=False`, input displacements are absolute.
-            If `increment=True`, input displacements are interpreted as increments of each other.
-            For example, an incremental input displacement of `[(2,0,0), (2,0,0), (2,0,0)]`
-            corresponds to an absolute input displacement of `[(2,0,0), (4,0,0), (6,0,0)]`.
-
-        Returns
-        -------
-        self: Magpylib object
-
-        Examples
-        --------
-
-        With the ``move`` method Magpylib objects can be repositioned in the global coordinate
-        system:
-
-        >>> import magpylib as magpy
-        >>> sensor = magpy.Sensor()
-        >>> print(sensor.position)
-        [0. 0. 0.]
-        >>> sensor.move((1,1,1))
-        >>> print(sensor.position)
-        [1. 1. 1.]
-
-        It is also a powerful tool for creating paths:
-
-        >>> import magpylib as magpy
-        >>> sensor = magpy.Sensor()
-        >>> sensor.move((1,1,1), start='append')
-        >>> print(sensor.position)
-        [[0. 0. 0.]
-         [1. 1. 1.]]
-        >>> sensor.move([(.1,.1,.1)]*2, start='append')
-        >>> print(sensor.position)
-        [[0.  0.  0. ]
-         [1.  1.  1. ]
-         [1.1 1.1 1.1]
-         [1.1 1.1 1.1]]
-
-        Complex paths can be generated with ease, by making use of the ``increment`` keyword
-        and superposition of subsequent paths:
-
-        >>> import magpylib as magpy
-        >>> sensor = magpy.Sensor()
-        >>> sensor.move([(1,1,1)]*4, start='append', increment=True)
-        >>> print(sensor.position)
-        [[0. 0. 0.]
-         [1. 1. 1.]
-         [2. 2. 2.]
-         [3. 3. 3.]
-         [4. 4. 4.]]
-        >>> sensor.move([(.1,.1,.1)]*5, start=2)
-        >>> print(sensor.position)
-        [[0.  0.  0. ]
-         [1.  1.  1. ]
-         [2.1 2.1 2.1]
-         [3.1 3.1 3.1]
-         [4.1 4.1 4.1]
-         [4.1 4.1 4.1]
-         [4.1 4.1 4.1]]
-
-        """
-
-        # check input types
-        if Config.checkinputs:
-            check_vector_type(displacement, "displacement")
-            check_start_type(start)
-            check_increment_type(increment)
-
-        # displacement vector -> ndarray
-        inpath = np.array(displacement, dtype=float)
-
-        # check input format
-        if Config.checkinputs:
-            check_path_format(inpath, "displacement")
-
-        # expand if input is shape (3,)
-        if inpath.ndim == 1:
-            inpath = np.expand_dims(inpath, 0)
-
-        # load old path
-        old_ppath = self._position
-        old_opath = self._orientation.as_quat()
-        lenop = len(old_ppath)
-        lenin = len(inpath)
-
-        # change start to positive values in [0, lenop]
-        start = adjust_start(start, lenop)
-
-        # incremental input -> absolute input
-        if increment:
-            for i, d in enumerate(inpath[:-1]):
-                inpath[i + 1] = inpath[i + 1] + d
-
-        end = start + lenin  # end position of new_path
-
-        til = end - lenop
-        if til > 0:  # case inpos extends beyond old_path -> tile up old_path
-            old_ppath = np.pad(old_ppath, ((0, til), (0, 0)), "edge")
-            old_opath = np.pad(old_opath, ((0, til), (0, 0)), "edge")
-            self.orientation = R.from_quat(old_opath)
-
-        # add new_ppath to old_ppath
-        old_ppath[start:end] += inpath
-        self.position = old_ppath
-
+        self.position = (0,0,0)
+        self.orientation = None
         return self
diff --git a/magpylib/_src/obj_classes/class_BaseRotation.py b/magpylib/_src/obj_classes/class_BaseTransform.py
similarity index 51%
rename from magpylib/_src/obj_classes/class_BaseRotation.py
rename to magpylib/_src/obj_classes/class_BaseTransform.py
index c973c0b55..f091fe63f 100644
--- a/magpylib/_src/obj_classes/class_BaseRotation.py
+++ b/magpylib/_src/obj_classes/class_BaseTransform.py
@@ -1,12 +1,13 @@
-"""BaseRotation class code"""
+"""BaseTransform class code"""
 # pylint: disable=too-many-instance-attributes
+# pylint: disable=protected-access
+
 import numpy as np
 from scipy.spatial.transform import Rotation as R
 from magpylib._src.exceptions import MagpylibBadUserInput
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import (
     check_start_type,
-    check_increment_type,
     check_rot_type,
     check_anchor_type,
     check_anchor_format,
@@ -15,19 +16,378 @@
     check_degree_type,
     check_angle_format,
     check_axis_format,
+    check_vector_type,
+    check_path_format,
 )
-from magpylib._src.utility import adjust_start
 
+def multi_anchor_behavior(anchor, inrotQ, rotation):
+    """
+    define behavior of rotation with given anchor
+
+    if one is longer than the other pad up other
+    """
+    len_inrotQ = 0 if inrotQ.ndim==1 else inrotQ.shape[0]
+    len_anchor = 0 if anchor.ndim==1 else anchor.shape[0]
+
+    if len_inrotQ>len_anchor:
+        if len_anchor==0:
+            anchor = np.reshape(anchor, (1,3))
+            len_anchor = 1
+        anchor = np.pad(anchor, ((0,len_inrotQ-len_anchor),(0,0)), 'edge')
+    elif len_inrotQ<len_anchor:
+        if len_inrotQ==0:
+            inrotQ = np.reshape(inrotQ, (1,4))
+            len_inrotQ = 1
+        inrotQ = np.pad(inrotQ, ((0,len_anchor-len_inrotQ),(0,0)), 'edge')
+        rotation = R.from_quat(inrotQ)
+
+    return anchor, inrotQ, rotation
+
+
+def path_padding_param(scalar_input: bool, lenop: int, lenip: int, start):
+    """
+    compute path padding parameters
+
+    Example: with start>0 input path exceeds old_path
+        old_path:            |abcdefg|
+        input_path:              |xzyuvwrst|
+        -> padded_old_path:  |abcdefggggggg|
+
+    Parameters:
+    -----------
+    scalar_input: True if rotation input is scalar, else False
+    lenop: length of old_path
+    lenip: length of input_path
+    start: start index
+
+    Returns:
+    --------
+    padding: (pad_before, pad_behind)
+        how much the old_path must be padded before
+    start: modified start value
+    """
+    # initialize paddings
+    pad_before = 0
+    pad_behind = 0
+
+    # start='auto': apply to all if scalar, append if vector
+    if start == "auto":
+        if scalar_input:
+            start = 0
+        else:
+            start = lenop
+
+    # numpy convention with negative start indices
+    if start < 0:
+        start = lenop + start
+        # if start smaller than -old_path_length: pad before
+        if start < 0:
+            pad_before = -start  # pylint: disable=invalid-unary-operand-type
+            start = 0
+
+    # vector: if start+inpath extends beyond oldpath: pad behind
+    if start + lenip > lenop + pad_before:
+        pad_behind = start + lenip - (lenop + pad_before)
+
+    if pad_before+pad_behind>0:
+        return (pad_before, pad_behind), start
+    return [], start
+
+
+def path_padding(inpath, start, target_object):
+    """
+    pad path of target_object and compute start- and end-index for apply_move()
+    and apply_rotation() functions below so that ppath[start:end] = X... can be
+    applied.
+
+    Parameters
+    ----------
+    inpath: user input as np.ndarray
+    start: start index
+    target_object: magpylib object with position and orientation attributes
+
+    Returns
+    -------
+    ppath: padded target_object position path
+    opath: padded target_object orientation path
+    start: modified start idex
+    end: end index
+    padded: True if padding was necessary, else False
+    """
+    # scalar or vector input
+    scalar_input = inpath.ndim == 1
+
+    # load old path
+    ppath = target_object._position
+    opath = target_object._orientation.as_quat()
+
+    lenip = 1 if scalar_input else len(inpath)
+
+    # pad old path depending on input
+    padding, start = path_padding_param(scalar_input, len(ppath), lenip, start)
+    if padding:
+        ppath = np.pad(ppath, (padding, (0, 0)), "edge")
+        opath = np.pad(opath, (padding, (0, 0)), "edge")
+
+    # set end-index
+    end = len(ppath) if scalar_input else start+lenip
+
+    return ppath, opath, start, end, bool(padding)
+
+
+def apply_move(target_object, displacement, start="auto"):
+    """
+    Implementation of the move() functionality.
+
+    Parameters
+    ----------
+    target_object: object with position and orientation attributes
+    displacement: displacement vector/path, array_like, shape (3,) or (n,3).
+        If the input is scalar (shape (3,)) the operation is applied to the
+        whole path. If the input is a vector (shape (n,3)), it is
+        appended/merged with the existing path.
+    start: int, str, default='auto'
+        start=i applies an operation starting at the i'th path index.
+        With start='auto' and scalar input the wole path is moved. With
+        start='auto' and vector input the input is appended.
+
+    Returns
+    -------
+    target_object
+    """
+    # pylint: disable=protected-access
+    # pylint: disable=attribute-defined-outside-init
+    # pylint: disable=too-many-branches
+
+    # check input types
+    if Config.checkinputs:
+        check_vector_type(displacement, "displacement")
+        check_start_type(start)
+
+    # displacement vector -> ndarray
+    inpath = np.array(displacement, dtype=float)
+
+    # check input format
+    if Config.checkinputs:
+        check_path_format(inpath, "displacement")
+
+    # pad target_object path and compute start and end-index for rotation application
+    ppath, opath, start, end, padded = path_padding(inpath, start, target_object)
+    if padded:
+        target_object._orientation = R.from_quat(opath)
+
+    # apply move operation
+    ppath[start:end] += inpath
+    target_object._position = ppath
+
+    return target_object
+
+
+def apply_rotation(
+    target_object,
+    rotation: R,
+    anchor=None,
+    start="auto",
+    parent_path=None):
+    """
+    Implementation of the rotate() functionality.
+
+    Parameters
+    ----------
+    target_object: object with position and orientation attributes
+    rotation: a scipy Rotation object
+        If the input is scalar (shape (3,)) the operation is applied to the
+        whole path. If the input is a vector (shape (n,3)), it is
+        appended/merged with the existing path.
+    anchor: array_like shape (3,)
+        Rotation anchor
+    start: int, str, default='auto'
+        start=i applies an operation starting at the i'th path index.
+        With start='auto' and scalar input the wole path is moved. With
+        start='auto' and vector input the input is appended.
+    parent_path=None if there is no parent else parent._position
+
+    Returns
+    -------
+    target_object
+    """
+    # pylint: disable=protected-access
+    # pylint: disable=too-many-branches
+
+    # check input types
+    if Config.checkinputs:
+        check_rot_type(rotation)
+        check_anchor_type(anchor)
+        check_start_type(start)
+
+    # input -> quaternion ndarray
+    inrotQ = rotation.as_quat()
+
+    # when an anchor is given
+    if anchor is not None:
+        # 0-anchor -> (0,0,0)
+        if np.isscalar(anchor) and anchor==0:
+            anchor = np.array((0.,0.,0.))
+        else:
+            anchor = np.array(anchor, dtype=float)
+        # check anchor input format
+        if Config.checkinputs:
+            check_anchor_format(anchor)
+        # apply multi-anchor behavior
+        anchor, inrotQ, rotation = multi_anchor_behavior(anchor, inrotQ, rotation)
+
+    # pad target_object path and compute start and end-index for rotation application
+    ppath, opath, newstart, end, _ = path_padding(inrotQ, start, target_object)
+
+    # compute anchor when dealing with Compound rotation (target_object is a child
+    #   that rotates about its parent). This happens when a rotation with anchor=None
+    #   is applied to a child in a Collection. In this case the anchor must be set to
+    #   the parent_path.
+    if anchor is None and parent_path is not None:
+        # target anchor length
+        len_anchor = end-newstart
+        # pad up parent_path if input requires it
+        padding, start = path_padding_param(inrotQ.ndim==1, parent_path.shape[0], len_anchor, start)
+        if padding:
+            parent_path = np.pad(parent_path, (padding, (0,0)), 'edge')
+        # slice anchor from padded parent_path
+        anchor = parent_path[start:start+len_anchor]
+
+    # position change when there is an anchor
+    if anchor is not None:
+        ppath[newstart:end] -= anchor
+        ppath[newstart:end] = rotation.apply(ppath[newstart:end])
+        ppath[newstart:end] += anchor
+
+    # set new rotation
+    oldrot = R.from_quat(opath[newstart:end])
+    opath[newstart:end] = (rotation * oldrot).as_quat()
+
+    # store new position and orientation
+    # pylint: disable=attribute-defined-outside-init
+    target_object._orientation = R.from_quat(opath)
+    target_object._position = ppath
+
+    return target_object
+
+class BaseTransform:
+    """
+    Inherit this class to provide rotation() and move() methods.
+
+    All rotate_from_XXX methods simply generate a scipy Rotation object and hand it
+    over to the main rotate() method. This then uses the apply_rotation function to
+    apply the rotations to all target objects.
+
+    - For Magpylib objects that inherit BaseRotate and BaseGeo (e.g. Cuboid()),
+      apply_rotation() is applied only to the object itself.
+    - Collections inherit only BaseRotate. In this case apply_rotation() is only
+      applied to the Collection children.
+    - Compounds are user-defined classes that inherit Collection but also inherit
+      BaseGeo. In this case apply_rotation() is applied to the object itself, but also
+      to its children.
+    """
+
+    def move(self, displacement, start="auto"):
+        """
+
+        Input Statement
+        If the input is a scalar the operation is applied to the whole path. If the
+        input is a vector, it is merged with the existing path.
+
+        Start Statement
+        start=i applies an operation starting at the i'th path index. start=None applies
+        an operation to the whole path.
+
+        Translates the object by the input displacement (can be a path).
+
+        This method uses vector addition to merge the input path given by displacement and the
+        existing old path of an object. It keeps the old orientation. If the input path extends
+        beyond the old path, the old path will be padded by its last entry before paths are
+        added up.
+
+        Parameters
+        ----------
+        displacement: array_like, shape (3,) or (N,3)
+            Displacement vector shape=(3,) or path shape=(N,3) in units of [mm].
+
+        start: int or str, default=-1
+            Choose at which index of the original object path, the input path will begin.
+            If `start=-1`, inp_path will start at the last old_path position.
+            If `start=0`, inp_path will start with the beginning of the old_path.
+            If `start=len(old_path)` or `start='append'`, inp_path will be attached to
+            the old_path.
+
+        Returns
+        -------
+        self: Magpylib object
 
-class BaseRotation:
-    """Defines the Rotation class for magpylib objects"""
+        Examples
+        --------
 
+        With the ``move`` method Magpylib objects can be repositioned in the global coordinate
+        system:
 
-    def __init__(self):
-        self._target_class = self
+        >>> import magpylib as magpy
+        >>> sensor = magpy.Sensor()
+        >>> print(sensor.position)
+        [0. 0. 0.]
+        >>> sensor.move((1,1,1))
+        >>> print(sensor.position)
+        [1. 1. 1.]
 
+        It is also a powerful tool for creating paths:
 
-    def rotate(self, rotation, anchor=None, start=-1, increment=False):
+        >>> import magpylib as magpy
+        >>> sensor = magpy.Sensor()
+        >>> sensor.move((1,1,1), start='append')
+        >>> print(sensor.position)
+        [[0. 0. 0.]
+         [1. 1. 1.]]
+        >>> sensor.move([(.1,.1,.1)]*2, start='append')
+        >>> print(sensor.position)
+        [[0.  0.  0. ]
+         [1.  1.  1. ]
+         [1.1 1.1 1.1]
+         [1.1 1.1 1.1]]
+
+        Complex paths can be generated with ease, by making use of the ``increment`` keyword
+        and superposition of subsequent paths:
+
+        >>> import magpylib as magpy
+        >>> sensor = magpy.Sensor()
+        >>> sensor.move([(1,1,1)]*4, start='append', increment=True)
+        >>> print(sensor.position)
+        [[0. 0. 0.]
+         [1. 1. 1.]
+         [2. 2. 2.]
+         [3. 3. 3.]
+         [4. 4. 4.]]
+        >>> sensor.move([(.1,.1,.1)]*5, start=2)
+        >>> print(sensor.position)
+        [[0.  0.  0. ]
+         [1.  1.  1. ]
+         [2.1 2.1 2.1]
+         [3.1 3.1 3.1]
+         [4.1 4.1 4.1]
+         [4.1 4.1 4.1]
+         [4.1 4.1 4.1]]
+        """
+
+        # Idea: An operation applied to a Collection is individually
+        #    applied to its BaseGeo and to each child.
+
+        for child in getattr(self, "children", []):
+            apply_move(child, displacement, start=start)
+
+        apply_move(self, displacement, start=start)
+
+        return self
+
+    def rotate(
+        self,
+        rotation: R,
+        anchor=None,
+        start="auto"):
         """
         Rotates object in the global coordinate system using a scipy Rotation object
         as input.
@@ -50,10 +410,6 @@ def rotate(self, rotation, anchor=None, start=-1, increment=False):
             If `start=len(old_path)` or `start='append'`, inp_path will be attached to
             the old_path.
 
-        increment: bool, default=False
-            If `increment=False`, input rotations are absolute.
-            If `increment=True`, input rotations are interpreted as increments of each other.
-
         Returns
         -------
         self: Magpylib object
@@ -77,105 +433,21 @@ def rotate(self, rotation, anchor=None, start=-1, increment=False):
         [0.70710678 0.70710678 0.        ]
         [ 0.  0. 45.]
         """
-        # pylint: disable=protected-access
-        if getattr(self, "_object_type", None) == "Collection":
-            for obj in self.objects:         # pylint: disable=no-member
-                self._target_class = obj
-                self._rotate(rotation, anchor, start, increment)
-            return self
-        return self._rotate(rotation, anchor, start, increment)
 
+        # pylint: disable=no-member
 
-    def _rotate(self, rotation, anchor=None, start=-1, increment=False):
-        """
-        rotate_from_XXX generates a scipy Rotation object R
-        then rotate() is called with R
-        then _rotate() is called from rotate() to enable Collection application
-
-        Inputs:
-        rotation: scipy rotation object
-        anchor: anchor point
-        start: int or 'append', path start position
-        increment: bool, interpretation of path-input
-
-        Rotates the object in the global coordinate system by a given rotation input
-        (can be a path).
-        """
+        # Idea: An operation applied to a Collection is individually
+        #    applied to its BaseGeo and to each child.
+        #  -> this automatically generates the rotate-Compound behavior
 
-        # check input types
-        if Config.checkinputs:
-            check_rot_type(rotation)
-            check_anchor_type(anchor)
-            check_start_type(start)
-            check_increment_type(increment)
+        for child in getattr(self, "children", []):
+            apply_rotation(child, rotation, anchor=anchor, start=start, parent_path=self._position)
 
-        # input anchor -> ndarray type
-        if anchor is not None:
-            anchor = np.array(anchor, dtype=float)
+        apply_rotation(self, rotation, anchor=anchor, start=start)
 
-        # check format
-        if Config.checkinputs:
-            check_anchor_format(anchor)
-            # Non need for Rotation check. R.as_quat() can only be of shape (4,) or (N,4)
-
-        # expand rot.as_quat() to shape (1,4)
-        rot = rotation
-        inrotQ = rot.as_quat()
-        if inrotQ.ndim == 1:
-            inrotQ = np.expand_dims(inrotQ, 0)
-            rot = R.from_quat(inrotQ)
-
-        # load old path
-        # pylint: disable=protected-access
-        old_ppath = self._target_class._position
-        old_opath = self._target_class._orientation.as_quat()
-
-        lenop = len(old_ppath)
-        lenin = len(inrotQ)
-
-        # change start to positive values in [0, lenop]
-        start = adjust_start(start, lenop)
-
-        # incremental input -> absolute input
-        #   missing Rotation object item assign to improve this code
-        if increment:
-            rot1 = rot[0]
-            for i, r in enumerate(rot[1:]):
-                rot1 = r * rot1
-                inrotQ[i + 1] = rot1.as_quat()
-            rot = R.from_quat(inrotQ)
-
-        end = start + lenin  # end position of new_path
-
-        # allocate new paths
-        til = end - lenop
-        if til <= 0:  # case inpos completely inside of existing path
-            new_ppath = old_ppath
-            new_opath = old_opath
-        else:  # case inpos extends beyond old_path -> tile up old_path
-            new_ppath = np.pad(old_ppath, ((0, til), (0, 0)), "edge")
-            new_opath = np.pad(old_opath, ((0, til), (0, 0)), "edge")
-
-        # position change when there is an anchor
-        if anchor is not None:
-            new_ppath[start:end] -= anchor
-            new_ppath[start:end] = rot.apply(new_ppath[start:end])
-            new_ppath[start:end] += anchor
-
-        # set new rotation
-        oldrot = R.from_quat(new_opath[start:end])
-        new_opath[start:end] = (rot * oldrot).as_quat()
-
-        # store new position and orientation
-        # pylint: disable=attribute-defined-outside-init
-        self._target_class.orientation = R.from_quat(new_opath)
-        self._target_class.position = new_ppath
-
-        return self._target_class
-
-
-    def rotate_from_angax(
-        self, angle, axis, anchor=None, start=-1, increment=False, degrees=True):
+        return self
+
+    def rotate_from_angax(self, angle, axis, anchor=None, start="auto", degrees=True):
         """
         Rotates object in the global coordinate system from angle-axis input.
 
@@ -201,10 +473,6 @@ def rotate_from_angax(
             If `start=len(old_path)` or `start='append'`, inp_path will be attached to
             the old_path.
 
-        increment: bool, default=False
-            If `increment=False`, input rotations are absolute.
-            If `increment=True`, input rotations are interpreted as increments of each other.
-
         degrees: bool, default=True
             By default angle is given in units of [deg]. If degrees=False, angle is given
             in units of [rad].
@@ -238,7 +506,6 @@ def rotate_from_angax(
             check_axis_type(axis)
             check_anchor_type(anchor)
             check_start_type(start)
-            check_increment_type(increment)
             check_degree_type(degrees)
 
         # generate axis from string
@@ -253,9 +520,10 @@ def rotate_from_angax(
                 else MagpylibBadUserInput(f'Bad axis string input "{axis}"')
             )
 
-        # input expand and ->ndarray
-        if isinstance(angle, (int, float)):
-            angle = (angle,)
+        # input is scalar or vector
+        is_scalar = isinstance(angle, (int, float))
+
+        # secure type - scalar angle will become a float
         angle = np.array(angle, dtype=float)
         axis = np.array(axis, dtype=float)
 
@@ -274,15 +542,18 @@ def rotate_from_angax(
             angle = angle / 180 * np.pi
 
         # apply rotation
-        angle = np.tile(angle, (3, 1)).T
+        if is_scalar:
+            angle = np.ones(3) * angle
+        else:
+            angle = np.tile(angle, (3, 1)).T
+
+        # generate rotation object from rotvec
         axis = axis / np.linalg.norm(axis)
         rot = R.from_rotvec(axis * angle)
 
-        return self.rotate(rot, anchor, start, increment)
+        return self.rotate(rot, anchor, start)
 
-
-    def rotate_from_rotvec(
-        self, rotvec, anchor=None, start=-1, increment=False, degrees=False):
+    def rotate_from_rotvec(self, rotvec, anchor=None, start="auto", degrees=False):
         """
         Rotates object in the global coordinate system from rotation vector input. (vector
         direction is the rotation axis, vector length is the rotation angle in [rad])
@@ -305,10 +576,6 @@ def rotate_from_rotvec(
             If `start=len(old_path)` or `start='append'`, inp_path will be attached to
             the old_path.
 
-        increment: bool, default=False
-            If `increment=False`, input rotations are absolute.
-            If `increment=True`, input rotations are interpreted as increments of each other.
-
         degrees : bool, default False
             If True, then the given angles are assumed to be in degrees.
 
@@ -335,11 +602,9 @@ def rotate_from_rotvec(
         [0. 0. 1.]
         """
         rot = R.from_rotvec(rotvec, degrees=degrees)
-        return self.rotate(rot, anchor=anchor, start=start, increment=increment)
-
+        return self.rotate(rot, anchor=anchor, start=start)
 
-    def rotate_from_euler(
-        self, seq, angles, anchor=None, start=-1, increment=False, degrees=False):
+    def rotate_from_euler(self, seq, angles, anchor=None, start="auto", degrees=False):
         """
         Rotates object in the global coordinate system from Euler angle input.
 
@@ -367,10 +632,6 @@ def rotate_from_euler(
             If `start=len(old_path)` or `start='append'`, inp_path will be attached to
             the old_path.
 
-        increment: bool, default=False
-            If `increment=False`, input rotations are absolute.
-            If `increment=True`, input rotations are interpreted as increments of each other.
-
         degrees : bool, default False
             If True, then the given angles are assumed to be in degrees.
 
@@ -397,10 +658,9 @@ def rotate_from_euler(
         [ 0.  0. 45.]
         """
         rot = R.from_euler(seq, angles, degrees=degrees)
-        return self.rotate(rot, anchor=anchor, start=start, increment=increment)
-
+        return self.rotate(rot, anchor=anchor, start=start)
 
-    def rotate_from_matrix(self, matrix, anchor=None, start=-1, increment=False):
+    def rotate_from_matrix(self, matrix, anchor=None, start="auto"):
         """
         Rotates object in the global coordinate system from matrix input.
         (see scipy rotation package matrix input)
@@ -423,10 +683,6 @@ def rotate_from_matrix(self, matrix, anchor=None, start=-1, increment=False):
             If `start=len(old_path)` or `start='append'`, inp_path will be attached to
             the old_path.
 
-        increment: bool, default=False
-            If `increment=False`, input rotations are absolute.
-            If `increment=True`, input rotations are interpreted as increments of each other.
-
         Returns
         -------
         self: Magpylib object
@@ -454,10 +710,9 @@ def rotate_from_matrix(self, matrix, anchor=None, start=-1, increment=False):
          [ 0.  0.  1.]]
         """
         rot = R.from_matrix(matrix)
-        return self.rotate(rot, anchor=anchor, start=start, increment=increment)
-
+        return self.rotate(rot, anchor=anchor, start=start)
 
-    def rotate_from_mrp(self, mrp, anchor=None, start=-1, increment=False):
+    def rotate_from_mrp(self, mrp, anchor=None, start="auto"):
         """
         Rotates object in the global coordinate system from Modified Rodrigues Parameters input.
         (see scipy rotation package Modified Rodrigues Parameters (MRPs))
@@ -479,10 +734,6 @@ def rotate_from_mrp(self, mrp, anchor=None, start=-1, increment=False):
             If `start=len(old_path)` or `start='append'`, inp_path will be attached to
             the old_path.
 
-        increment: bool, default=False
-            If `increment=False`, input rotations are absolute.
-            If `increment=True`, input rotations are interpreted as increments of each other.
-
         Returns
         -------
         self: Magpylib object
@@ -506,10 +757,9 @@ def rotate_from_mrp(self, mrp, anchor=None, start=-1, increment=False):
         [0. 0. 1.]
         """
         rot = R.from_mrp(mrp)
-        return self.rotate(rot, anchor=anchor, start=start, increment=increment)
-
+        return self.rotate(rot, anchor=anchor, start=start)
 
-    def rotate_from_quat(self, quat, anchor=None, start=-1, increment=False):
+    def rotate_from_quat(self, quat, anchor=None, start="auto"):
         """
         Rotates object in the global coordinate system from Quaternion input.
 
@@ -532,10 +782,6 @@ def rotate_from_quat(self, quat, anchor=None, start=-1, increment=False):
             If `start=len(old_path)` or `start='append'`, inp_path will be attached to
             the old_path.
 
-        increment: bool, default=False
-            If `increment=False`, input rotations are absolute.
-            If `increment=True`, input rotations are interpreted as increments of each other.
-
         Returns
         -------
         self: Magpylib object
@@ -559,4 +805,4 @@ def rotate_from_quat(self, quat, anchor=None, start=-1, increment=False):
         [0.         0.         0.70710678 0.70710678]
         """
         rot = R.from_quat(quat)
-        return self.rotate(rot, anchor=anchor, start=start, increment=increment)
+        return self.rotate(rot, anchor=anchor, start=start)
diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py
index e44496ddd..1e3f863f0 100644
--- a/magpylib/_src/obj_classes/class_Collection.py
+++ b/magpylib/_src/obj_classes/class_Collection.py
@@ -4,35 +4,35 @@
     format_obj_input,
     check_duplicates,
     LIBRARY_SENSORS,
-    LIBRARY_SOURCES,
-)
+    LIBRARY_SOURCES)
+
+from magpylib._src.obj_classes.class_BaseGeo import BaseGeo
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
-from magpylib._src.obj_classes.class_BaseRotation import BaseRotation
 from magpylib._src.fields.field_wrap_BH_level2 import getBH_level2
-from magpylib._src.default_utils import validate_style_keys
+from magpylib._src.defaults.defaults_utility import validate_style_keys
 from magpylib._src.exceptions import MagpylibBadUserInput
 
-# ON INTERFACE
-class Collection(BaseDisplayRepr, BaseRotation):
+
+class BaseCollection(BaseDisplayRepr):
     """
-    Group multiple objects in one Collection for common manipulation.
+    Group multiple children in one Collection for common manipulation.
 
-    Operations applied to a Collection are sequentially applied to all objects in the Collection.
-    Collections do not allow duplicate objects (will be eliminated automatically).
+    Operations applied to a Collection are sequentially applied to all children in the Collection.
+    Collections do not allow duplicate children (will be eliminated automatically).
 
     Collections have the following dunders defined: __add__, __sub__, __iter__, __getitem__,
     __repr__.
 
-    Depending on the input, Collection objects can be sources, observers or both. A Collection
-    with only source objects will become either a SourceCollection, one with only Sensors a
+    Depending on the input, Collection children can be sources, observers or both. A Collection
+    with only source children will become either a SourceCollection, one with only Sensors a
     SensorCollection, and one with sources and sensors a MixedCollection
     A SourceCollection functions like any SINGLE source. A SensorCollection functions like a
     list of observer inputs. A MixedCollection will function as source or as observer.
 
     Parameters
     ----------
-    objects: sources, sensors, collections or arbitrary lists thereof
-        Ordered list of objects in the Collection.
+    children: sources, sensors, collections or arbitrary lists thereof
+        Ordered list of children in the Collection.
 
     Returns
     -------
@@ -41,8 +41,8 @@ class Collection(BaseDisplayRepr, BaseRotation):
     Examples
     --------
 
-    Create Collections for common manipulation. All objects added to a Collection
-    are stored in the ``objects`` attribute, and additionally in the ``sensors``
+    Create Collections for common manipulation. All children added to a Collection
+    are stored in the ``children`` attribute, and additionally in the ``sensors``
     and ``sources`` attributes. These three return ordered sets (lists with
     unique elements only)
 
@@ -51,10 +51,10 @@ class Collection(BaseDisplayRepr, BaseRotation):
     >>> loop = magpy.current.Loop(1,1)
     >>> dipole = magpy.misc.Dipole((1,2,3))
     >>> col = magpy.Collection(sphere, loop, dipole)
-    >>> print(col.objects)
+    >>> print(col.children)
     [Sphere(id=1879891544384), Loop(id=1879891543040), Dipole(id=1879892157152)]
 
-    Cycle directly through the Collection ``objects`` attribute
+    Cycle directly through the Collection ``children`` attribute
 
     >>> for src in col:
     >>>    print(src)
@@ -62,38 +62,22 @@ class Collection(BaseDisplayRepr, BaseRotation):
     Loop(id=1879891543040)
     Dipole(id=1879892157152)
 
-    and directly access objects from the Collection
+    and directly access children from the Collection
 
     >>> print(col[1])
     Loop(id=1879891543040)
 
-    Add and subtract objects to form a Collection and to remove objects from a Collection.
+    Add and subtract children to form a Collection and to remove children from a Collection.
 
     >>> col = sphere + loop
-    >>> print(col.objects)
+    >>> print(col.children)
     [Sphere(id=1879891544384), Loop(id=1879891543040)]
     >>> col - sphere
-    >>> print(col.objects)
+    >>> print(col.children)
     [Loop(id=1879891543040)]
 
-    Manipulate all objects in a Collection directly using ``move`` and ``rotate`` methods
-
-    >>> import magpylib as magpy
-    >>> sphere = magpy.magnet.Sphere((1,2,3),1)
-    >>> loop = magpy.current.Loop(1,1)
-    >>> col = sphere + loop
-    >>> col.move((1,1,1))
-    >>> print(sphere.position)
-    [1. 1. 1.]
-
-    and compute the total magnetic field generated by the Collection.
-
-    >>> B = col.getB((1,2,3))
-    >>> print(B)
-    [-0.00372678  0.01820438  0.03423079]
-
     Consider three collections, a SourceCollection sCol a SensorCollection xCol and a
-    MixedCollection mCol, all made up from the same objects.
+    MixedCollection mCol, all made up from the same children.
 
     >>> import numpy as np
     >>> import magpylib as magpy
@@ -122,31 +106,30 @@ class Collection(BaseDisplayRepr, BaseRotation):
     >>> mCol.getB()
     """
 
-    def __init__(self, *objects):
+    def __init__(self, *children):
 
         self._object_type = "Collection"
 
         # init inheritance
         BaseDisplayRepr.__init__(self)
-        BaseRotation.__init__(self)
 
         # instance attributes
-        self._objects = []
+        self._children = []
         self._sources = []
         self._sensors = []
-        self.objects = objects
+        self.children = children
 
     # property getters and setters
     @property
-    def objects(self):
-        """Collection objects attribute getter and setter."""
-        return self._objects
-
-    @objects.setter
-    def objects(self, objects):
-        """Set Collection objects."""
-        obj_list = format_obj_input(objects, allow="sources+sensors")
-        self._objects = []
+    def children(self):
+        """Collection children attribute getter and setter."""
+        return self._children
+
+    @children.setter
+    def children(self, children):
+        """Set Collection children."""
+        obj_list = format_obj_input(children, allow="sources+sensors")
+        self._children = []
         self.add(obj_list)
 
     @property
@@ -158,7 +141,7 @@ def sources(self):
     def sources(self, sources):
         """Set Collection sources."""
         src_list = format_obj_input(sources, allow="sources")
-        self._objects = [o for o in self._objects if o not in self._sources]
+        self._children = [o for o in self._children if o not in self._sources]
         self.add(src_list)
 
     @property
@@ -170,28 +153,21 @@ def sensors(self):
     def sensors(self, sensors):
         """Set Collection sensors."""
         sens_list = format_obj_input(sensors, allow="sensors")
-        self._objects = [o for o in self._objects if o not in self._sensors]
+        self._children = [o for o in self._children if o not in self._sensors]
         self.add(sens_list)
 
     # dunders
-    def __add__(self, obj):
-        if obj._object_type == "Collection":
-            new_obj = Collection(self, obj)
-        else:
-            new_obj = self.add(obj)
-        return new_obj
-
     def __sub__(self, obj):
         return self.remove(obj)
 
     def __iter__(self):
-        yield from self._objects
+        yield from self._children
 
     def __getitem__(self, i):
-        return self._objects[i]
+        return self._children[i]
 
     def __len__(self):
-        return len(self._objects)
+        return len(self._children)
 
     def __repr__(self) -> str:
         # pylint: disable=protected-access
@@ -205,15 +181,15 @@ def __repr__(self) -> str:
         return f"{pref}{s}"
 
     # methods -------------------------------------------------------
-    def add(self, *objects):
+    def add(self, *children):
         """
-        Add arbitrary Magpylib objects or Collections.
+        Add arbitrary Magpylib children or Collections.
 
         Parameters
         ----------
-        objects: sources, Sensors, Collections or arbitrary lists thereof
-            Add arbitrary sequences of objects and Collections to the Collection.
-            The new objects will be added at the end of self.objects. Duplicates
+        children: sources, Sensors, Collections or arbitrary lists thereof
+            Add arbitrary sequences of children and Collections to the Collection.
+            The new children will be added at the end of self.children. Duplicates
             will be eliminated.
 
         Returns
@@ -223,45 +199,45 @@ def add(self, *objects):
         Examples
         --------
 
-        Add objects to a Collection:
+        Add children to a Collection:
 
         >>> import magpylib as magpy
         >>> src = magpy.current.Loop(1,1)
         >>> col = magpy.Collection()
         >>> col.add(src)
-        >>> print(col.objects)
+        >>> print(col.children)
         [Loop(id=2519738714432)]
 
         """
         # format input
-        obj_list = format_obj_input(objects)
+        obj_list = format_obj_input(children)
         # combine with original obj_list
-        obj_list = self._objects + obj_list
+        obj_list = self._children + obj_list
         # check and eliminate duplicates
         obj_list = check_duplicates(obj_list)
         # set attributes
-        self._objects = obj_list
+        self._children = obj_list
         self._update_src_and_sens()
         return self
 
     def _update_src_and_sens(self):
         # pylint: disable=protected-access
-        """updates source and sensor list when an object is added or removed"""
+        """updates source and sensor list when a child is added or removed"""
         self._sources = [
-            obj for obj in self._objects if obj._object_type in LIBRARY_SOURCES
+            obj for obj in self._children if obj._object_type in LIBRARY_SOURCES
         ]
         self._sensors = [
-            obj for obj in self._objects if obj._object_type in LIBRARY_SENSORS
+            obj for obj in self._children if obj._object_type in LIBRARY_SENSORS
         ]
 
-    def remove(self, obj):
+    def remove(self, child):
         """
-        Remove a specific object from the Collection.
+        Remove a specific child from the Collection.
 
         Parameters
         ----------
-        object: object object
-            Remove the given object from the Collection.
+        child: child object
+            Remove the given child from the Collection.
 
         Returns
         -------
@@ -269,82 +245,23 @@ def remove(self, obj):
 
         Examples
         --------
-        Remove a specific object from a Collection:
+        Remove a specific child from a Collection:
 
         >>> import magpylib as magpy
         >>> src1 = magpy.current.Loop(1,1)
         >>> src2 = magpy.current.Loop(1,1)
         >>> col = src1 + src2
-        >>> print(col.objects)
+        >>> print(col.children)
         [Loop(id=2405009623360), Loop(id=2405010235504)]
         >>> col.remove(src1)
-        >>> print(col.objects)
+        >>> print(col.children)
         [Loop(id=2405010235504)]
 
         """
-        self._objects.remove(obj)
+        self._children.remove(child)
         self._update_src_and_sens()
         return self
 
-    def move(self, displacement, start=-1, increment=False):
-        """
-        Translates each object in the Collection individually by the input displacement
-        (can be a path).
-
-        This method uses vector addition to merge the input path given by displacement and the
-        existing old path of an object. It keeps the old orientation. If the input path extends
-        beyond the old path, the old path will be padded by its last entry before paths are
-        added up.
-
-        Parameters
-        ----------
-        displacement: array_like, shape (3,) or (N,3)
-            Displacement vector shape=(3,) or path shape=(N,3) in units of [mm].
-
-        start: int or str, default=-1
-            Choose at which index of the original object path, the input path will begin.
-            If `start=-1`, inp_path will start at the last old_path position.
-            If `start=0`, inp_path will start with the beginning of the old_path.
-            If `start=len(old_path)` or `start='append'`, inp_path will be attached to
-            the old_path.
-
-        increment: bool, default=False
-            If `increment=False`, input displacements are absolute.
-            If `increment=True`, input displacements are interpreted as increments of each other.
-            For example, an incremental input displacement of `[(2,0,0), (2,0,0), (2,0,0)]`
-            corresponds to an absolute input displacement of `[(2,0,0), (4,0,0), (6,0,0)]`.
-
-        Returns
-        -------
-        self: Collection
-
-        Examples
-        --------
-        This method will apply the ``move`` operation to each Collection object individually.
-
-        >>> import magpylib as magpy
-        >>> dipole = magpy.misc.Dipole((1,2,3))
-        >>> loop = magpy.current.Loop(1,1)
-        >>> col = loop + dipole
-        >>> col.move([(1,1,1), (2,2,2)])
-        >>> for src in col:
-        >>>     print(src.position)
-        [[1. 1. 1.]  [2. 2. 2.]]
-        [[1. 1. 1.]  [2. 2. 2.]]
-
-        But manipulating individual objects keeps them in the Collection
-
-        >>> dipole.move((1,1,1))
-        >>> for src in col:
-        >>>     print(src.position)
-        [[1. 1. 1.]  [2. 2. 2.]]
-        [[1. 1. 1.]  [3. 3. 3.]]
-
-        """
-        for s in self:
-            s.move(displacement, start, increment)
-        return self
-
     def copy(self):
         """
         Returns a copy of the Collection.
@@ -355,7 +272,7 @@ def copy(self):
 
         Examples
         --------
-        Create a copy of a Collection object:
+        Create a deep copy of the current Collection:
 
         >>> import magpylib as magpy
         >>> col = magpy.Collection()
@@ -368,39 +285,10 @@ def copy(self):
         """
         return copy.copy(self)
 
-    def reset_path(self):
-        """
-        Reset all object paths to position = (0,0,0) and orientation = unit rotation.
-
-        Returns
-        -------
-        self: Collection
-
-        Examples
-        --------
-        Create a collection with non-zero paths
-
-        >>> import magpylib as magpy
-        >>> dipole = magpy.misc.Dipole((1,2,3), position=(1,2,3))
-        >>> loop = magpy.current.Loop(1,1, position=[(1,1,1)]*2)
-        >>> col = loop + dipole
-        >>> for src in col:
-        >>>     print(src.position)
-        [[1. 1. 1.]  [1. 1. 1.]]
-        [1. 2. 3.]
-        >>> col.reset_path()
-        >>> for src in col:
-        >>>     print(src.position)
-        [0. 0. 0.]
-        [0. 0. 0.]
-        """
-        for obj in self:
-            obj.reset_path()
-        return self
 
-    def set_styles(self, arg=None, **kwargs):
+    def set_children_styles(self, arg=None, **kwargs):
         """
-        Set display style of all objects in the Collection. Only matching properties
+        Set display style of all children in the Collection. Only matching properties
         will be applied. Input can be a style-dict or style-underscore_magic.
 
         Returns
@@ -409,7 +297,7 @@ def set_styles(self, arg=None, **kwargs):
 
         Examples
         --------
-        Apply a style to all objects inside a Collection using a style-dict or
+        Apply a style to all children inside a Collection using a style-dict or
         style-underscore_magic.
 
         >>> import magpylib as magpy
@@ -423,11 +311,11 @@ def set_styles(self, arg=None, **kwargs):
         >>> src = magpy.magnet.Sphere((1,1,1), 1, (3,0,0))
         >>>
         >>> # set collection style
-        >>> col.set_styles(color='g')
+        >>> col.set_children_styles(color='g')
         >>>
         >>> # set collection style with style-dict
         >>> style_dict = {'magnetization_size':0.5}
-        >>> col.set_styles(style_dict)
+        >>> col.set_children_styles(style_dict)
         >>>
         >>> magpy.display(col, src)
         ---> graphic output
@@ -438,7 +326,7 @@ def set_styles(self, arg=None, **kwargs):
         if kwargs:
             arg.update(kwargs)
         style_kwargs = validate_style_keys(arg)
-        for src in self._objects:
+        for src in self._children:
             # match properties false will try to apply properties from kwargs only if it finds it
             # withoug throwing an error
             style_kwargs_specific = {
@@ -449,31 +337,31 @@ def set_styles(self, arg=None, **kwargs):
             src.style.update(**style_kwargs_specific, _match_properties=True)
         return self
 
-    def _validate_getBH_inputs(self, *objects):
+    def _validate_getBH_inputs(self, *children):
         # pylint: disable=too-many-branches
         """validate Collection.getBH inputs"""
         # pylint: disable=protected-access
         sources, sensors = list(self._sources), list(self._sensors)
         if self._sensors and self._sources:
             sources, sensors = self, self
-            if objects:
+            if children:
                 raise MagpylibBadUserInput(
                     "No inputs allowed for a Mixed Collection, "
                     "since it already has Sensors and Sources"
                 )
         elif not sources:
-            sources, sensors = objects, self
+            sources, sensors = children, self
         elif not sensors:
-            sources, sensors = self, objects
+            sources, sensors = self, children
         return sources, sensors
 
-    def getB(self, *objects, sumup=False, squeeze=True):
+    def getB(self, *children, sumup=False, squeeze=True):
         """
         Compute B-field in [mT] for given sources and observers.
 
         Parameters
         ----------
-        objects: source or observer objects
+        children: source or observer children
             If parent is a SourceCollection, input can only be M observers.
             If parent is a SensorCollection, input can only be L sources.
 
@@ -488,24 +376,24 @@ def getB(self, *objects, sumup=False, squeeze=True):
         -------
         B-field: ndarray, shape squeeze(L, M, N1, N2, ..., 3)
             B-field of each source (L) at each path position (M) and each sensor pixel
-            position (N1,N2,...) in units of [mT]. Paths of objects that are shorter than
+            position (N1,N2,...) in units of [mT]. Paths of children that are shorter than
             M will be considered as static beyond their end.
 
         Examples
         --------
         """
 
-        sources, sensors = self._validate_getBH_inputs(*objects)
+        sources, sensors = self._validate_getBH_inputs(*children)
 
         return getBH_level2(True, sources, sensors, sumup, squeeze)
 
-    def getH(self, *objects, sumup=False, squeeze=True):
+    def getH(self, *children, sumup=False, squeeze=True):
         """
         Compute H-field in [kA/m] for given sources and observers.
 
         Parameters
         ----------
-        objects: source or observer objects
+        children: source or observer children
             If parent is a SourceCollection, input can only be M observers.
             If parent is a SensorCollection, input can only be L sources.
 
@@ -520,13 +408,127 @@ def getH(self, *objects, sumup=False, squeeze=True):
         -------
         H-field: ndarray, shape squeeze(L, M, N1, N2, ..., 3)
             H-field of each source (L) at each path position (M) and each sensor pixel
-            position (N1,N2,...) in units of [kA/m]. Paths of objects that are shorter than
+            position (N1,N2,...) in units of [kA/m]. Paths of children that are shorter than
             M will be considered as static beyond their end.
 
         Examples
         --------
         """
 
-        sources, sensors = self._validate_getBH_inputs(*objects)
+        sources, sensors = self._validate_getBH_inputs(*children)
 
         return getBH_level2(False, sources, sensors, sumup, squeeze)
+
+
+class Collection(BaseGeo, BaseCollection):
+    """
+    Group multiple children in one Collection for common manipulation.
+
+    Operations applied to a Collection are sequentially applied to all children in the Collection.
+    Collections do not allow duplicate children (will be eliminated automatically).
+
+    Collections have the following dunders defined: __add__, __sub__, __iter__, __getitem__,
+    __repr__.
+
+    Depending on the input, Collection children can be sources, observers or both. A Collection
+    with only source children will become either a SourceCollection, one with only Sensors a
+    SensorCollection, and one with sources and sensors a MixedCollection
+    A SourceCollection functions like any SINGLE source. A SensorCollection functions like a
+    list of observer inputs. A MixedCollection will function as source or as observer.
+
+    Parameters
+    ----------
+    children: sources, sensors, collections or arbitrary lists thereof
+        Ordered list of children in the Collection.
+
+    Returns
+    -------
+    Collection object: Collection
+
+    Examples
+    --------
+
+    Create Collections for common manipulation. All children added to a Collection
+    are stored in the ``children`` attribute, and additionally in the ``sensors``
+    and ``sources`` attributes. These three return ordered sets (lists with
+    unique elements only)
+
+    >>> import magpylib as magpy
+    >>> sphere = magpy.magnet.Sphere((1,2,3),1)
+    >>> loop = magpy.current.Loop(1,1)
+    >>> dipole = magpy.misc.Dipole((1,2,3))
+    >>> col = magpy.Collection(sphere, loop, dipole)
+    >>> print(col.children)
+    [Sphere(id=1879891544384), Loop(id=1879891543040), Dipole(id=1879892157152)]
+
+    Cycle directly through the Collection ``children`` attribute
+
+    >>> for src in col:
+    >>>    print(src)
+    Sphere(id=1879891544384)
+    Loop(id=1879891543040)
+    Dipole(id=1879892157152)
+
+    and directly access children from the Collection
+
+    >>> print(col[1])
+    Loop(id=1879891543040)
+
+    Add and subtract children to form a Collection and to remove children from a Collection.
+
+    >>> col = sphere + loop
+    >>> print(col.children)
+    [Sphere(id=1879891544384), Loop(id=1879891543040)]
+    >>> col - sphere
+    >>> print(col.children)
+    [Loop(id=1879891543040)]
+
+    Manipulate all children in a Collection directly using ``move`` and ``rotate`` methods
+
+    >>> import magpylib as magpy
+    >>> sphere = magpy.magnet.Sphere((1,2,3),1)
+    >>> loop = magpy.current.Loop(1,1)
+    >>> col = sphere + loop
+    >>> col.move((1,1,1))
+    >>> print(sphere.position)
+    [1. 1. 1.]
+
+    and compute the total magnetic field generated by the Collection.
+
+    >>> B = col.getB((1,2,3))
+    >>> print(B)
+    [-0.00372678  0.01820438  0.03423079]
+
+    Consider three collections, a SourceCollection sCol a SensorCollection xCol and a
+    MixedCollection mCol, all made up from the same children.
+
+    >>> import numpy as np
+    >>> import magpylib as magpy
+
+    >>> s1=magpy.magnet.Sphere((1,2,3), 1)
+    >>> s2=magpy.magnet.Cylinder((1,2,3), (1,1), (3,0,0))
+    >>> s3=magpy.magnet.Cuboid((1,2,3), (1,1,1), (6,0,0))
+
+    >>> x1=magpy.Sensor((1,0,3))
+    >>> x2=magpy.Sensor((4,0,3))
+    >>> x3=magpy.Sensor((7,0,3))
+
+    >>> sCol = magpy.Collection(s1, s2, s3)
+    >>> xCol = magpy.Collection(x1, x2, x3)
+    >>> mCol = magpy.Collection(sCol, xCol)
+
+    All the following lines will all give the same output
+
+    >>> magpy.getB([s1,s2,s3], [x1,x2,x3], sumup=True)
+    >>> magpy.getB(sCol, xCol)
+    >>> magpy.getB(mCol, mCol)
+    >>> sCol.getB(xCol)
+    >>> xCol.getB(sCol)
+    >>> sCol.getB(mCol)
+    >>> xCol.getB(mCol)
+    >>> mCol.getB()
+    """
+
+    def __init__(self, *args, position=(0.0, 0.0, 0.0), orientation=None, style=None):
+        BaseGeo.__init__(self, position=position, orientation=orientation, style=style)
+        BaseCollection.__init__(self, *args)
diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py
index 289675900..85511b0e5 100644
--- a/magpylib/_src/obj_classes/class_Sensor.py
+++ b/magpylib/_src/obj_classes/class_Sensor.py
@@ -3,7 +3,7 @@
 from magpylib._src.obj_classes.class_BaseGeo import BaseGeo
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.utility import format_star_input
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_vector_type, check_position_format
 from magpylib._src.fields.field_wrap_BH_level2 import getBH_level2
 
diff --git a/magpylib/_src/obj_classes/class_current_Line.py b/magpylib/_src/obj_classes/class_current_Line.py
index 4c53e2085..e8d8c1fa1 100644
--- a/magpylib/_src/obj_classes/class_current_Line.py
+++ b/magpylib/_src/obj_classes/class_current_Line.py
@@ -5,7 +5,7 @@
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.obj_classes.class_BaseGetBH import BaseGetBH
 from magpylib._src.obj_classes.class_BaseExcitations import BaseCurrent
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_vertex_format, check_vector_type
 
 # init for tool tips
diff --git a/magpylib/_src/obj_classes/class_current_Loop.py b/magpylib/_src/obj_classes/class_current_Loop.py
index 9c4a3b0d7..e3cee81da 100644
--- a/magpylib/_src/obj_classes/class_current_Loop.py
+++ b/magpylib/_src/obj_classes/class_current_Loop.py
@@ -4,7 +4,7 @@
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.obj_classes.class_BaseGetBH import BaseGetBH
 from magpylib._src.obj_classes.class_BaseExcitations import BaseCurrent
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_scalar_type
 
 # init for tool tips
diff --git a/magpylib/_src/obj_classes/class_mag_Cuboid.py b/magpylib/_src/obj_classes/class_mag_Cuboid.py
index b9db1b59a..dbbad2cdc 100644
--- a/magpylib/_src/obj_classes/class_mag_Cuboid.py
+++ b/magpylib/_src/obj_classes/class_mag_Cuboid.py
@@ -5,7 +5,7 @@
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.obj_classes.class_BaseGetBH import BaseGetBH
 from magpylib._src.obj_classes.class_BaseExcitations import BaseHomMag
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_vector_format, check_vector_type
 
 # init for tool tips
diff --git a/magpylib/_src/obj_classes/class_mag_Cylinder.py b/magpylib/_src/obj_classes/class_mag_Cylinder.py
index d8d06141b..26a4f5575 100644
--- a/magpylib/_src/obj_classes/class_mag_Cylinder.py
+++ b/magpylib/_src/obj_classes/class_mag_Cylinder.py
@@ -5,7 +5,7 @@
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.obj_classes.class_BaseGetBH import BaseGetBH
 from magpylib._src.obj_classes.class_BaseExcitations import BaseHomMag
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_vector_type, check_vector_format
 
 # init for tool tips
diff --git a/magpylib/_src/obj_classes/class_mag_CylinderSegment.py b/magpylib/_src/obj_classes/class_mag_CylinderSegment.py
index 3446fd186..2f57af65d 100644
--- a/magpylib/_src/obj_classes/class_mag_CylinderSegment.py
+++ b/magpylib/_src/obj_classes/class_mag_CylinderSegment.py
@@ -5,7 +5,7 @@
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.obj_classes.class_BaseGetBH import BaseGetBH
 from magpylib._src.obj_classes.class_BaseExcitations import BaseHomMag
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_input_cyl_sect, check_vector_type
 
 # init for tool tips
diff --git a/magpylib/_src/obj_classes/class_mag_Sphere.py b/magpylib/_src/obj_classes/class_mag_Sphere.py
index b5fb80c5f..eb732c1cd 100644
--- a/magpylib/_src/obj_classes/class_mag_Sphere.py
+++ b/magpylib/_src/obj_classes/class_mag_Sphere.py
@@ -4,7 +4,7 @@
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.obj_classes.class_BaseGetBH import BaseGetBH
 from magpylib._src.obj_classes.class_BaseExcitations import BaseHomMag
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_scalar_type
 
 # init for tool tips
diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py
index 8f4da84cf..ecf7028c0 100644
--- a/magpylib/_src/obj_classes/class_misc_Dipole.py
+++ b/magpylib/_src/obj_classes/class_misc_Dipole.py
@@ -4,7 +4,7 @@
 from magpylib._src.obj_classes.class_BaseGeo import BaseGeo
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.obj_classes.class_BaseGetBH import BaseGetBH
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_vector_format, check_vector_type
 
 # init for tool tips
diff --git a/magpylib/_src/obj_classes/display.py b/magpylib/_src/obj_classes/display.py
new file mode 100644
index 000000000..dc4cd93d5
--- /dev/null
+++ b/magpylib/_src/obj_classes/display.py
@@ -0,0 +1,353 @@
+""" Display function codes"""
+
+import warnings
+from itertools import cycle
+import numpy as np
+import matplotlib.pyplot as plt
+from magpylib._src.utility import format_obj_input, test_path_format
+from magpylib._src.style import get_style
+from magpylib._src.display.display_utility import MagpyMarkers
+
+from magpylib._src.display.display_matplotlib import (
+    draw_directs_faced,
+    draw_faces,
+    draw_markers,
+    draw_path,
+    draw_pixel,
+    draw_sensors,
+    draw_dipoles,
+    draw_circular,
+    draw_line,
+    draw_model3d_extra,
+)
+from magpylib._src.display.display_utility import (
+    faces_cuboid,
+    faces_cylinder,
+    system_size,
+    faces_sphere,
+    faces_cylinder_section,
+)
+from magpylib._src.input_checks import check_excitations, check_dimensions
+from magpylib._src.defaults.defaults_classes import default_settings as Config
+
+
+# ON INTERFACE
+def display(
+    *objects,
+    path=True,  # bool, int, index list, 'animate'
+    zoom=0,
+    animate_time=3,
+    markers=None,
+    backend=None,
+    canvas=None,
+    **kwargs,
+):
+    """
+    Display objects and paths graphically. Style input can be a style-dict or
+    style-underscore_magic.
+
+    Parameters
+    ----------
+    objects: sources, collections or sensors
+        Objects to be displayed.
+
+    path: bool or int i or array_like shape (n,) or `'animate'`, default = True
+        Option False shows objects at final path position and hides paths.
+        Option True shows objects at final path position and shows object paths.
+        Option int i displays the objects at every i'th path position.
+        Option array_like shape (n,) discribes certain path indices. The objects
+        displays are displayed at every given path index.
+        Option `'animate'` (Plotly backend only) shows an animation of objectes moving
+        along their paths.
+
+    zoom: float, default = 0
+        Adjust plot zoom-level. When zoom=0 3D-figure boundaries are tight.
+
+    animate_time: float, default = 3
+        Sets the animation duration.
+
+    markers: array_like, shape (N,3), default=None
+        Display position markers in the global CS. By default no marker is displayed.
+
+    backend: string, default=None
+        One of `'matplotlib'` or `'plotly'`. If not set, parameter will default to
+        magpylib.defaults.display.backend which comes as 'matplotlib' with installation.
+
+    canvas: pyplot Axis or plotly Figure, default=None
+        Display graphical output in a given canvas:
+        - with matplotlib: pyplot axis (must be 3D).
+        - with plotly: plotly.graph_objects.Figure or plotly.graph_objects.FigureWidget
+        By default a new canvas is created and displayed.
+
+    Returns
+    -------
+    None: NoneType
+
+    Examples
+    --------
+
+    Display multiple objects, object paths, markers in 3D using Matplotlib or Plotly:
+
+    >>> import magpylib as magpy
+    >>> src = magpy.magnet.Sphere(magnetization=(0,0,1), diameter=1)
+    >>> src.move([(.1,0,0)]*50, increment=True)
+    >>> src.rotate_from_angax(angle=[10]*50, axis='z', anchor=0, start=0, increment=True)
+    >>> ts = [-.4,0,.4]
+    >>> sens = magpy.Sensor(position=(0,0,2), pixel=[(x,y,0) for x in ts for y in ts])
+    >>> magpy.display(src, sens)
+    >>> magpy.display(src, sens, backend='plotly')
+    --> graphic output
+
+    Display figure on your own canvas (here Matplotlib 3D axis):
+
+    >>> import matplotlib.pyplot as plt
+    >>> import magpylib as magpy
+    >>> my_axis = plt.axes(projection='3d')
+    >>> magnet = magpy.magnet.Cuboid(magnetization=(1,1,1), dimension=(1,2,3))
+    >>> sens = magpy.Sensor(position=(0,0,3))
+    >>> magpy.display(magnet, sens, canvas=my_axis, zoom=1)
+    >>> plt.show()
+    --> graphic output
+
+    Use sophisticated figure styling options accessible from defaults, as individual object styles
+    or as global style arguments in display.
+
+    >>> import magpylib as magpy
+    >>> src1 = magpy.magnet.Sphere((1,1,1), 1)
+    >>> src2 = magpy.magnet.Sphere((1,1,1), 1, (1,0,0))
+    >>> magpy.defaults.display.style.magnet.magnetization.size = 2
+    >>> src1.style.magnetization.size = 1
+    >>> magpy.display(src1, src2, style_color='r', zoom=3)
+    --> graphic output
+    """
+
+    # flatten input
+    obj_list_flat = format_obj_input(objects, allow='sources+sensors')
+    obj_list_semi_flat = format_obj_input(objects, allow='sources+sensors+collections')
+
+    # test if all source dimensions and excitations (if sho_direc=True) have been initialized
+    check_dimensions(obj_list_flat)
+
+    # test if every individual obj_path is good
+    test_path_format(obj_list_flat)
+    check_show_path = (
+        isinstance(path, (int, bool))
+        or path == "animate"
+        or (hasattr(path, "__iter__") and not isinstance(path, str))
+    )
+    assert check_show_path, (
+        f"`path` argument of type {type(path)} is invalid, \n"
+        "it must be one of (True, False, 'animate'), a positive path index "
+        "or an Iterable of path indices."
+    )
+
+    if backend is None:
+        backend = Config.display.backend
+
+    if backend == "matplotlib":
+        if path == "animate":
+            warnings.warn(
+                "The matplotlib backend does not support animation, falling back to path=True"
+            )
+            path = True
+        display_matplotlib(
+            *obj_list_semi_flat,
+            markers=markers,
+            path=path,
+            zoom=zoom,
+            axis=canvas,
+            **kwargs,
+        )
+    elif backend == "plotly":
+        # pylint: disable=import-outside-toplevel
+        from magpylib._src.display.plotly.plotly_display import display_plotly
+
+        display_plotly(
+            *obj_list_semi_flat,
+            markers=markers,
+            show_path=path,
+            zoom=zoom,
+            fig=canvas,
+            animate_time=animate_time,
+            **kwargs,
+        )
+
+
+def display_matplotlib(
+    *obj_list_semi_flat,
+    axis=None,
+    markers=None,
+    path=True,
+    zoom=0,
+    color_sequence=None,
+    **kwargs,
+):
+    """
+    Display objects and paths graphically with the matplotlib backend.
+
+    - axis: matplotlib axis3d object
+    - markers: list of marker positions
+    - path: bool / int / list of ints
+    - zoom: zoom level, 0=tight boundaries
+    - color_sequence: list of colors for object coloring
+    """
+    # pylint: disable=protected-access
+    # pylint: disable=too-many-branches
+    # pylint: disable=too-many-statements
+
+    # apply config default values if None
+    if color_sequence is None:
+        color_sequence = Config.display.colorsequence
+    # create or set plotting axis
+    if axis is None:
+        fig = plt.figure(dpi=80, figsize=(8, 8))
+        ax = fig.add_subplot(111, projection="3d")
+        ax.set_box_aspect((1, 1, 1))
+        generate_output = True
+    else:
+        ax = axis
+        generate_output = False
+
+    # draw objects and evaluate system size --------------------------------------
+
+    # draw faced objects and store vertices
+    points = []
+    dipoles_color = []
+    sensors_color = []
+    faced_objects_color = []
+
+    for semi_flat_obj, color in zip(obj_list_semi_flat, cycle(color_sequence)):
+        flat_objs = [semi_flat_obj]
+        if getattr(semi_flat_obj, "children", None) is not None:
+            flat_objs = semi_flat_obj.children
+            if getattr(semi_flat_obj, "position", None) is not None:
+                flat_objs += [semi_flat_obj]
+                color = color if semi_flat_obj.style.color is None else semi_flat_obj.style.color
+        for obj in flat_objs:
+            style = get_style(obj, Config, **kwargs)
+            color = style.color if style.color is not None else color
+            lw = 0.25
+            faces = None
+            if obj.style.model3d.extra:
+                draw_model3d_extra(obj, style, path, ax, color)
+            if obj.style.model3d.show:
+                if obj._object_type == "Cuboid":
+                    lw = 0.5
+                    faces = faces_cuboid(obj, path)
+                elif obj._object_type == "Cylinder":
+                    faces = faces_cylinder(obj, path)
+                elif obj._object_type == "CylinderSegment":
+                    faces = faces_cylinder_section(obj, path)
+                elif obj._object_type == "Sphere":
+                    faces = faces_sphere(obj, path)
+                elif obj._object_type == "Line":
+                    if style.arrow.show:
+                        check_excitations([obj])
+                    arrow_size = style.arrow.size if style.arrow.show else 0
+                    arrow_width = style.arrow.width
+                    points += draw_line(
+                        [obj], path, color, arrow_size, arrow_width, ax
+                    )
+                elif obj._object_type == "Loop":
+                    if style.arrow.show:
+                        check_excitations([obj])
+                    arrow_width = style.arrow.width
+                    arrow_size = style.arrow.size if style.arrow.show else 0
+                    points += draw_circular(
+                        [obj], path, color, arrow_size, arrow_width, ax
+                    )
+                elif obj._object_type == "Sensor":
+                    sensors_color += [color]
+                    points += draw_pixel(
+                        [obj],
+                        ax,
+                        color,
+                        style.pixel.color,
+                        style.pixel.size,
+                        style.pixel.symbol,
+                        path,
+                    )
+                elif obj._object_type == "Dipole":
+                    dipoles_color += [color]
+                    points += [obj.position]
+                elif obj._object_type == "CustomSource":
+                    draw_markers(np.array([obj.position]), ax, color, symbol="*", size=10)
+                    name = (
+                        obj.style.name
+                        if obj.style.name is not None
+                        else str(type(obj).__name__)
+                    )
+                    ax.text(*obj.position, name, horizontalalignment="center")
+                    points += [obj.position]
+                if faces is not None:
+                    faced_objects_color += [color]
+                    alpha = style.opacity
+                    pts = draw_faces(faces, color, lw, alpha, ax)
+                    points += [np.vstack(pts).reshape(-1, 3)]
+                    if style.magnetization.show:
+                        check_excitations([obj])
+                        pts = draw_directs_faced(
+                            [obj], [color], ax, path, style.magnetization.size
+                        )
+                        points += pts
+            if path:
+                marker, line = style.path.marker, style.path.line
+                if style.path.show:
+                    points += draw_path(
+                        obj,
+                        color,
+                        marker.symbol,
+                        marker.size,
+                        marker.color,
+                        line.style,
+                        line.width,
+                        ax,
+                    )
+
+    # markers -------------------------------------------------------
+    if markers is not None and markers:
+        m = MagpyMarkers()
+        style = get_style(m, Config, **kwargs)
+        markers = np.array(markers)
+        s = style.marker
+        draw_markers(markers, ax, s.color, s.symbol, s.size)
+        points += [markers]
+
+    # draw direction arrows (based on src size) -------------------------
+    # objects with faces
+
+    # determine system size -----------------------------------------
+    limx1, limx0, limy1, limy0, limz1, limz0 = system_size(points)
+
+    # make sure ranges are not null
+    limits = np.array([[limx0, limx1], [limy0, limy1], [limz0, limz1]])
+    limits[np.squeeze(np.diff(limits)) == 0] += np.array([-1, 1])
+    sys_size = np.max(np.diff(limits))
+    c = limits.mean(axis=1)
+    m = sys_size.max() / 2
+    ranges = np.array([c - m * (1 + zoom), c + m * (1 + zoom)]).T
+
+    # draw all system sized based quantities -------------------------
+    # sensors
+    sensors = [obj for obj in obj_list_semi_flat if obj._object_type == "Sensor"]
+
+    # dipoles
+    dipoles = [obj for obj in obj_list_semi_flat if obj._object_type == "Dipole"]
+
+    # not optimal for loop if many sensors/dipoles
+    for sensor in sensors:
+        style = get_style(sensor, Config, **kwargs)
+        draw_sensors([sensor], ax, sys_size, path, style.size)
+    for dipole, color in zip(dipoles, dipoles_color):
+        style = get_style(dipole, Config, **kwargs)
+        draw_dipoles([dipole], ax, sys_size, path, style.size, color, style.pivot)
+
+    # plot styling --------------------------------------------------
+    ax.set(
+        **{f"{k}label": f"{k} [mm]" for k in "xyz"},
+        **{f"{k}lim": r for k, r in zip("xyz", ranges)},
+    )
+
+    # generate output ------------------------------------------------
+    if generate_output:
+        plt.show()
diff --git a/magpylib/_src/style.py b/magpylib/_src/style.py
index ba8aaf44d..b54359370 100644
--- a/magpylib/_src/style.py
+++ b/magpylib/_src/style.py
@@ -1,7 +1,7 @@
 """Collection of classes for display styling"""
 # pylint: disable=C0302
 
-from magpylib._src.default_utils import (
+from magpylib._src.defaults.defaults_utility import (
     MagicProperties,
     validate_property_class,
     color_validator,
@@ -230,7 +230,11 @@ def text(self):
 
     @text.setter
     def text(self, val):
-        self._text = val if val is None else str(val)
+        assert val is None or isinstance(val, str), (
+            f"the `show` property of {type(self).__name__} must be a string\n"
+            f"but received {repr(val)} instead"
+        )
+        self._text = val
 
     @property
     def show(self):
diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py
index 331b0439f..d4deb3c21 100644
--- a/magpylib/_src/utility.py
+++ b/magpylib/_src/utility.py
@@ -1,11 +1,12 @@
 """ some utility functions"""
+from math import log10
 from typing import Sequence
 import numpy as np
 
 # from scipy.spatial.transform import Rotation as R
 from magpylib._src.exceptions import MagpylibBadUserInput
 from magpylib import _src
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 from magpylib._src.input_checks import check_position_format
 
 LIBRARY_SOURCES = (
@@ -76,7 +77,8 @@ def format_star_input(inp):
     return list(inp)
 
 
-def format_obj_input(objects: Sequence, allow="sources+sensors", warn=True) -> list:
+def format_obj_input(
+    *objects: Sequence, allow="sources+sensors", warn=True) -> list:
     """tests and flattens potential input sources (sources, Collections, sequences)
 
     ### Args:
@@ -91,20 +93,24 @@ def format_obj_input(objects: Sequence, allow="sources+sensors", warn=True) -> l
     # pylint: disable=protected-access
 
     obj_list = []
+    flatten_collection = not 'collections' in allow.split("+")
     for obj in objects:
         if isinstance(obj, (tuple, list)):
             obj_list += format_obj_input(
-                obj, allow=allow, warn=warn
+                *obj, allow=allow, warn=warn
             )  # recursive flattening
         else:
             try:
                 if obj._object_type == "Collection":
-                    obj_list += obj.objects
+                    if flatten_collection:
+                        obj_list += obj.children
+                    else:
+                        obj_list += [obj]
                 elif obj._object_type in list(LIBRARY_SOURCES) + list(LIBRARY_SENSORS):
                     obj_list += [obj]
             except Exception as error:
                 raise MagpylibBadUserInput(wrong_obj_msg(obj, allow=allow)) from error
-    obj_list = filter_objects(obj_list, allow=allow, warn=warn)
+    obj_list = filter_objects(obj_list, allow=allow, warn=False)
     return obj_list
 
 
@@ -264,6 +270,8 @@ def filter_objects(obj_list, allow="sources+sensors", warn=True):
             allowed_list.extend(LIBRARY_SOURCES)
         elif allowed == "sensors":
             allowed_list.extend(LIBRARY_SENSORS)
+        elif allowed =='collections':
+            allowed_list.extend(['Collection'])
     new_list = []
     for obj in obj_list:
         if obj._object_type in allowed_list:
@@ -273,24 +281,54 @@ def filter_objects(obj_list, allow="sources+sensors", warn=True):
                 print(f"Warning, cannot add {obj.__repr__()} to Collection.")
     return new_list
 
-def adjust_start(start, lenop):
+
+_UNIT_PREFIX = {
+    -24: "y",  # yocto
+    -21: "z",  # zepto
+    -18: "a",  # atto
+    -15: "f",  # femto
+    -12: "p",  # pico
+    -9: "n",  # nano
+    -6: "µ",  # micro
+    -3: "m",  # milli
+    0: "",
+    3: "k",  # kilo
+    6: "M",  # mega
+    9: "G",  # giga
+    12: "T",  # tera
+    15: "P",  # peta
+    18: "E",  # exa
+    21: "Z",  # zetta
+    24: "Y",  # yotta
+}
+
+
+def unit_prefix(number, unit="", precision=3, char_between="") -> str:
     """
-    change start to a value inside of [0,lenop], i.e. inside of the
-    old path.
+    displays a number with given unit and precision and uses unit prefixes for the exponents from
+    yotta (y) to Yocto (Y). If the exponent is smaller or bigger, falls back to scientific notation.
+
+    Parameters
+    ----------
+    number : int, float
+        can be any number
+    unit : str, optional
+        unit symbol can be any string, by default ""
+    precision : int, optional
+        gives the number of significant digits, by default 3
+    char_between : str, optional
+        character to insert between number of prefix. Can be " " or any string, if a space is wanted
+        before the unit symbol , by default ""
+
+    Returns
+    -------
+    str
+        returns formatted number as string
     """
-    if start == "append":
-        start = lenop
-    elif start < 0:
-        start += lenop
-
-    # fix out-of-bounds start values
-    if start < 0:
-        start = 0
-        if Config.checkinputs:
-            print("Warning: start out of path bounds. Setting start=0.")
-    elif start > lenop:
-        start = lenop
-        if Config.checkinputs:
-            print(f"Warning: start out of path bounds. Setting start={lenop}.")
-
-    return start
+    digits = int(log10(abs(number))) // 3 * 3 if number != 0 else 0
+    prefix = _UNIT_PREFIX.get(digits, "")
+
+    if prefix == "":
+        digits = 0
+    new_number_str = f"{number / 10 ** digits:.{precision}g}"
+    return f"{new_number_str}{char_between}{prefix}{unit}"
diff --git a/setup.py b/setup.py
index 5cc6e152c..b153d9722 100644
--- a/setup.py
+++ b/setup.py
@@ -70,6 +70,7 @@ def run(self):
     # kaleido, jupyterlab are needed for testing with display(renderer='json', backend='plotly')
     extras_require={
         "testing": [
+            "numpy>=1.20",
             "plotly>=5.3",
             "kaleido",
             "pytest",
diff --git a/tox.ini b/tox.ini
index 515414f22..3245ceea8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py38
+envlist = py39
 
 [testenv]
 # install pytest in the virtualenv where commands will be executed


Test patch:
diff --git a/tests/test_BaseTransform.py b/tests/test_BaseTransform.py
new file mode 100644
index 000000000..a8bba9bae
--- /dev/null
+++ b/tests/test_BaseTransform.py
@@ -0,0 +1,395 @@
+import pytest
+import numpy as np
+from scipy.spatial.transform import Rotation as R
+import magpylib as magpy
+from magpylib._src.obj_classes.class_BaseTransform import apply_rotation, apply_move
+
+
+@pytest.mark.parametrize(
+    ("description", "old_position", "displacement", "new_position", "start",),
+    [
+        # SCALAR INPUT
+        ("01_ with start='auto'", (0, 0, 0), (1, 2, 3), (1, 2, 3), "auto"),
+        ("02_ with start=0", (1, 2, 3), (1, 2, 3), (2, 4, 6), 0),
+        ("03_ with start=-1", (2, 4, 6), (1, 2, 3), (3, 6, 9), -1),
+        ("04_ pad behind", (3, 6, 9), (-1, -2, -3), [(3, 6, 9), (2, 4, 6)], 1),
+        (
+            "05_ whole path",
+            [(3, 6, 9), (2, 4, 6)],
+            (-1, -2, -3),
+            [(2, 4, 6), (1, 2, 3)],
+            "auto",
+        ),
+        (
+            "06_ pad before",
+            [(2, 4, 6), (1, 2, 3)],
+            (-1, -2, -3),
+            [(1, 2, 3), (1, 2, 3), (0, 0, 0)],
+            -3,
+        ),
+        (
+            "07_ whole path starting in the middle",
+            [(1, 2, 3), (1, 2, 3), (0, 0, 0)],
+            (1, 2, 3),
+            [(1, 2, 3), (2, 4, 6), (1, 2, 3)],
+            1,
+        ),
+        (
+            "08_ whole path starting in the middle with negative start",
+            [(1, 2, 3), (2, 4, 6), (1, 2, 3)],
+            (1, 2, 3),
+            [(1, 2, 3), (3, 6, 9), (2, 4, 6)],
+            -2,
+        ),
+        # VECTOR INPUT
+        (
+            "17_ vector + start=0: simple append",
+            (0, 0, 0),
+            [(1, 2, 3)],
+            [(0, 0, 0), (1, 2, 3)],
+            "auto",
+        ),
+        (
+            "18_ vector + start in middle: merge",
+            [(0, 0, 0), (1, 2, 3)],
+            [(1, 2, 3)],
+            [(0, 0, 0), (2, 4, 6)],
+            1,
+        ),
+        (
+            "19_ vector + start in middle: merge + pad behind",
+            [(0, 0, 0), (2, 4, 6)],
+            [(-1, -2, -3), (-2, -4, -6)],
+            [(0, 0, 0), (1, 2, 3), (0, 0, 0)],
+            1,
+        ),
+        (
+            "20_ vector + start before: merge + pad before",
+            [(0, 0, 0), (1, 2, 3), (0, 0, 0)],
+            [(1, 2, 3), (1, 2, 3)],
+            [(1, 2, 3), (1, 2, 3), (1, 2, 3), (0, 0, 0)],
+            -4,
+        ),
+    ],
+)
+def test_apply_move(description, old_position, displacement, new_position, start):
+    """ v4 path functionality tests """
+    print(description)
+    s = magpy.Sensor(position=old_position)
+    apply_move(s, displacement, start=start)
+    assert np.all(s.position == np.array(new_position))
+
+
+@pytest.mark.parametrize(
+    (
+        "description",
+        "old_position",
+        "new_position",
+        "old_orientation_rotvec",
+        "rotvec_to_apply",
+        "new_orientation_rotvec",
+        "start",
+        "anchor",
+    ),
+    [
+        # SCALAR INPUT
+        (
+            "01_ with start='auto'",
+            (0, 0, 0),
+            (0, 0, 0),
+            (0, 0, 0),
+            (0.1, 0.2, 0.3),
+            (0.1, 0.2, 0.3),
+            "auto",
+            None,
+        ),
+        (
+            "02_ with start=0",
+            (0, 0, 0),
+            (0, 0, 0),
+            (0.1, 0.2, 0.3),
+            (0.1, 0.2, 0.3),
+            (0.2, 0.4, 0.6),
+            0,
+            None,
+        ),
+        (
+            "03_ with start=-1",
+            (0, 0, 0),
+            (0, 0, 0),
+            (0.2, 0.4, 0.6),
+            (-0.2, -0.4, -0.6),
+            (0, 0, 0),
+            -1,
+            None,
+        ),
+        (
+            "04_  with anchor",
+            (0, 0, 0),
+            (1, -1, 0),
+            (0, 0, 0),
+            (0, 0, np.pi / 2),
+            (0, 0, np.pi / 2),
+            -1,
+            (1, 0, 0),
+        ),
+        (
+            "05_  pad behind",
+            (1, -1, 0),
+            [(1, -1, 0), (2, 0, 0)],
+            (0, 0, np.pi / 2),
+            (0, 0, np.pi / 2),
+            [(0, 0, np.pi / 2), (0, 0, np.pi)],
+            1,
+            (1, 0, 0),
+        ),
+        (
+            "06_  whole path",
+            [(1, -1, 0), (2, 0, 0)],
+            [(2, 0, 0), (1, 1, 0)],
+            [(0, 0, np.pi / 2), (0, 0, np.pi)],
+            (0, 0, np.pi / 2),
+            [(0, 0, np.pi), (0, 0, -np.pi / 2)],
+            "auto",
+            (1, 0, 0),
+        ),
+        (
+            "07_ pad before",
+            [(2, 0, 0), (1, 1, 0)],
+            [(1, 1, 0), (1, 1, 0), (0, 0, 0)],
+            [(0, 0, np.pi), (0, 0, -np.pi / 2)],
+            (0, 0, np.pi / 2),
+            [(0, 0, -np.pi / 2), (0, 0, -np.pi / 2), (0, 0, 0)],
+            -3,
+            (1, 0, 0),
+        ),
+        (
+            "08_ whole path starting in the middle",
+            [(1, 1, 0), (1, 1, 0), (0, 0, 0)],
+            [(1, 1, 0), (0, 0, 0), (1, -1, 0)],
+            [(0, 0, -np.pi / 2), (0, 0, -np.pi / 2), (0, 0, 0)],
+            (0, 0, np.pi / 2),
+            [(0, 0, -np.pi / 2), (0, 0, 0), (0, 0, np.pi / 2)],
+            1,
+            (1, 0, 0),
+        ),
+        (
+            "09_ whole path starting in the middle without anchor",
+            [(1, 1, 0), (0, 0, 0), (1, -1, 0)],
+            [(1, 1, 0), (0, 0, 0), (1, -1, 0)],
+            [(0, 0, -np.pi / 2), (0, 0, 0), (0, 0, np.pi / 2)],
+            ((0, 0, np.pi / 4)),
+            [(0, 0, -np.pi / 2), (0, 0, np.pi / 4), (0, 0, 3 * np.pi / 4)],
+            1,
+            None,
+        ),
+        # VECTOR INPUT
+        (
+            "11_ simple append start=auto behavior",
+            (0, 0, 0),
+            [(0, 0, 0), (1, -1, 0)],
+            (0, 0, 0),
+            [(0, 0, np.pi / 2)],
+            [(0, 0, 0), (0, 0, np.pi / 2)],
+            "auto",
+            (1, 0, 0),
+        ),
+        (
+            "12_ vector + start=0: simple merge",
+            [(0, 0, 0), (1, -1, 0)],
+            [(1, -1, 0), (1, -1, 0)],
+            [(0, 0, 0), (0, 0, np.pi / 2)],
+            [(0, 0, np.pi / 2)],
+            [(0, 0, np.pi / 2), (0, 0, np.pi / 2)],
+            0,
+            (1, 0, 0),
+        ),
+        (
+            "13_ vector + start in middle: merge + pad behind",
+            [(1, -1, 0), (1, -1, 0)],
+            [(1, -1, 0), (1, 1, 0), (1, 1, 0)],
+            [(0, 0, np.pi / 2), (0, 0, np.pi / 2)],
+            [(0, 0, np.pi), (0, 0, np.pi)],
+            [(0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, -np.pi / 2)],
+            1,
+            (1, 0, 0),
+        ),
+        (
+            "14_ vector + start before: merge + pad before",
+            [(1, -1, 0), (1, 1, 0), (1, 1, 0)],
+            [(1, -1, 0), (1, 1, 0), (1, 1, 0), (1, 1, 0)],
+            [(0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, -np.pi / 2)],
+            [(0, 0, 0), (0, 0, np.pi)],
+            [
+                (0, 0, np.pi / 2),
+                (0, 0, -np.pi / 2),
+                (0, 0, -np.pi / 2),
+                (0, 0, -np.pi / 2),
+            ],
+            -4,
+            (1, 0, 0),
+        ),
+    ],
+)
+def test_apply_rotation(
+    description,
+    old_position,
+    new_position,
+    old_orientation_rotvec,
+    rotvec_to_apply,
+    new_orientation_rotvec,
+    start,
+    anchor,
+):
+    """ v4 path functionality tests """
+    print(description)
+    s = magpy.Sensor(
+        position=old_position, orientation=R.from_rotvec(old_orientation_rotvec)
+    )
+    apply_rotation(s, R.from_rotvec(rotvec_to_apply), start=start, anchor=anchor)
+    assert np.allclose(s.position, np.array(new_position))
+    assert np.allclose(
+        s.orientation.as_matrix(), R.from_rotvec(new_orientation_rotvec).as_matrix()
+    )
+
+
+# def test_apply_move_v4_pt1():
+#     """ v4 path functionality tests """
+#     # pylint: disable=too-many-statements
+
+#     # SCALAR INPUT - ABSOLUTE=FALSE
+#     s = magpy.Sensor()
+
+#     # move object with start='auto'
+#     apply_move(s, (1,2,3))
+#     assert np.all(s.position == (1,2,3))
+
+#     # move object with start=0
+#     apply_move(s, (1,2,3), start=0)
+#     assert np.all(s.position == (2,4,6))
+
+#     # move object with start=-1
+#     apply_move(s, (1,2,3), start=-1)
+#     assert np.all(s.position == (3,6,9))
+
+#     # pad behind
+#     apply_move(s, (-1,-2,-3), start=1)
+#     assert np.all(s.position == [(3,6,9), (2,4,6)])
+
+#     # move whole path
+#     apply_move(s, (-1,-2,-3))
+#     assert np.all(s.position == [(2,4,6), (1,2,3)])
+
+#     # pad before
+#     apply_move(s, (-1,-2,-3), start=-3)
+#     assert np.all(s.position == [(1,2,3), (1,2,3), (0,0,0)])
+
+#     # move whole path starting in the middle
+#     apply_move(s, (1,2,3), start=1)
+#     assert np.all(s.position == [(1,2,3), (2,4,6), (1,2,3)])
+
+#     # move whole path starting in the middle with negative start
+#     apply_move(s, (1,2,3), start=-2)
+#     assert np.all(s.position == [(1,2,3), (3,6,9), (2,4,6)])
+
+
+# def test_apply_move_v4_pt3():
+#     """ v4 path functionality tests """
+#     # VECTOR INPUT - ABSOLUTE=FALSE
+#     s = magpy.Sensor()
+
+#     # vector + start=0: simple append
+#     apply_move(s, [(1,2,3)])
+#     assert np.all(s.position == [(0,0,0), (1,2,3)])
+
+#     # vector + start in middle: merge
+#     apply_move(s, [(1,2,3)], start=1)
+#     assert np.all(s.position == [(0,0,0), (2,4,6)])
+
+#     # vector + start in middle: merge + pad behind
+#     apply_move(s, [(-1,-2,-3), (-2,-4,-6)], start=1)
+#     assert np.all(s.position == [(0,0,0), (1,2,3), (0,0,0)])
+
+#     # vector + start before: merge + pad before
+#     apply_move(s, [(1,2,3), (1,2,3)], start=-4)
+#     assert np.all(s.position == [(1,2,3), (1,2,3), (1,2,3), (0,0,0)])
+
+
+# def test_apply_rotation_v4_pt1():
+#     """ v4 path functionality tests """
+
+#     # SCALAR INPUT
+#     s = magpy.Sensor()
+#     # rotate object with start='auto'
+#     apply_rotation(s, R.from_rotvec((.1,.2,.3)))
+
+#     assert np.allclose(s.position, (0,0,0))
+#     assert np.allclose(s.orientation.as_rotvec(), (.1,.2,.3))
+
+#     # rotate object with start=0
+#     apply_rotation(s, R.from_rotvec((.1,.2,.3)), start=0)
+#     assert np.allclose(s.position, (0,0,0))
+#     assert np.allclose(s.orientation.as_rotvec(), (.2,.4,.6))
+
+#     # rotate object with start=-1
+#     apply_rotation(s, R.from_rotvec((-.2,-.4,-.6)), start=-1)
+#     assert np.allclose(s.position, (0,0,0))
+#     assert np.allclose(s.orientation.as_rotvec(), (0,0,0))
+
+#     # rotate object with anchor
+#     apply_rotation(s, R.from_rotvec((0,0,np.pi/2)), anchor=(1,0,0))
+#     assert np.allclose(s.position, (1,-1,0))
+#     assert np.allclose(s.orientation.as_rotvec(), (0,0,np.pi/2))
+
+#     # pad behind
+#     apply_rotation(s, R.from_rotvec((0,0,np.pi/2)), anchor=(1,0,0), start=1)
+#     assert np.allclose(s.position, ((1,-1,0), (2,0,0)))
+#     assert np.allclose(s.orientation.as_rotvec(), ((0,0,np.pi/2), (0,0,np.pi)))
+
+#     # rotate whole path
+#     apply_rotation(s, R.from_rotvec((0,0,np.pi/2)), anchor=(1,0,0))
+#     assert np.allclose(s.position, ((2,0,0), (1,1,0)))
+#     assert np.allclose(s.orientation.as_rotvec(), ((0,0,np.pi), (0,0,-np.pi/2)))
+
+#     # pad before
+#     apply_rotation(s, R.from_rotvec((0,0,np.pi/2)), anchor=(1,0,0), start=-3)
+#     assert np.allclose(s.position, ((1,1,0), (1,1,0), (0,0,0)))
+#     assert np.allclose(s.orientation.as_rotvec(), ((0,0,-np.pi/2), (0,0,-np.pi/2), (0,0,0)))
+
+#     # rotate whole path starting in the middle
+#     apply_rotation(s, R.from_rotvec((0,0,np.pi/2)), anchor=(1,0,0), start=1)
+#     assert np.allclose(s.position, ((1,1,0), (0,0,0), (1,-1,0)))
+#     assert np.allclose(s.orientation.as_rotvec(), ((0,0,-np.pi/2), (0,0,0), (0,0,np.pi/2)))
+
+#     # rotate whole path starting in the middle without anchor
+#     apply_rotation(s, R.from_rotvec((0,0,np.pi/4)), start=1)
+#     assert np.allclose(s.position, ((1,1,0), (0,0,0), (1,-1,0)))
+#     assert np.allclose(s.orientation.as_rotvec(), ((0,0,-np.pi/2), (0,0,np.pi/4), (0,0,3*np.pi/4)))
+
+
+# def test_apply_rotation_v4_pt2():
+#     """ v4 path functionality tests """
+#     # VECTOR INPUT - ABSOLUTE=FALSE
+#     s = magpy.Sensor()
+
+#     # simple append start=auto behavior
+#     apply_rotation(s, R.from_rotvec(((0,0,np.pi/2),)), anchor=(1,0,0))
+#     assert np.allclose(s.position, ((0,0,0), (1,-1,0)))
+#     assert np.allclose(s.orientation.as_rotvec(), ((0,0,0), (0,0,np.pi/2)))
+
+#     # vector + start=0: simple merge
+#     apply_rotation(s, R.from_rotvec(((0,0,np.pi/2),)), anchor=(1,0,0), start=0)
+#     assert np.allclose(s.position, ((1,-1,0), (1,-1,0)))
+#     assert np.allclose(s.orientation.as_rotvec(), ((0,0,np.pi/2), (0,0,np.pi/2)))
+
+#     # vector + start in middle: merge + pad behind
+#     apply_rotation(s, R.from_rotvec(((0,0,np.pi), (0,0,np.pi))), anchor=(1,0,0), start=1)
+#     assert np.allclose(s.position, ((1,-1,0), (1,1,0), (1,1,0)))
+#     assert np.allclose(s.orientation.as_rotvec(), ((0,0,np.pi/2), (0,0,-np.pi/2), (0,0,-np.pi/2)))
+
+
+#     # vector + start before: merge + pad before
+#     apply_rotation(s, R.from_rotvec(((0,0,0), (0,0,np.pi))), anchor=(1,0,0), start=-4)
+#     assert np.allclose(s.position, ((1,-1,0), (1,1,0), (1,1,0), (1,1,0)))
+#     assert np.allclose(
+#         s.orientation.as_rotvec(), ((0,0,np.pi/2), (0,0,-np.pi/2), (0,0,-np.pi/2), (0,0,-np.pi/2)))
diff --git a/tests/test_Coumpound_setters.py b/tests/test_Coumpound_setters.py
new file mode 100644
index 000000000..3e6932987
--- /dev/null
+++ b/tests/test_Coumpound_setters.py
@@ -0,0 +1,150 @@
+
+# pylint: disable=eval-used
+# pylint: disable=unused-import
+import os
+
+import pytest
+from scipy.spatial.transform.rotation import Rotation as R
+import numpy as np
+import plotly.graph_objects as go
+import magpylib as magpy
+from magpylib._src.display.plotly.plotly_base_traces import make_BasePrism
+
+magpy.defaults.display.backend = "plotly"
+
+
+def make_wheel(Ncubes=6, height=10, diameter=36, path_len=5, name=None):
+    """creates a basic Collection Compound object with a rotary arrangement of cuboid magnets"""
+    cs_lambda = lambda: magpy.magnet.Cuboid(
+        (1, 0, 0), dimension=[height] * 3, position=(diameter / 2, 0, 0)
+    )
+    s0 = cs_lambda().rotate_from_angax(
+        np.linspace(0.0, 360.0, Ncubes, endpoint=False), "z", anchor=(0, 0, 0), start=0
+    )
+    cs_list = []
+    for ind in range(Ncubes):
+        s = cs_lambda()
+        s.position = s0.position[ind]
+        s.orientation = s0.orientation[ind]
+        cs_list.append(s)
+    c = magpy.Collection(cs_list)
+    c.rotate_from_angax(90, "x")
+    c.rotate_from_angax(
+        np.linspace(90, 360, path_len), axis="z", start=0, anchor=(80, 0, 0)
+    )
+    c.move(np.linspace((0, 0, 0), (0, 0, 200), path_len), start=0)
+    c.style.name = name
+
+    trace = make_BasePrism(
+        base_vertices=Ncubes, diameter=diameter + height * 2, height=height * 0.5
+    )
+    trace_plotly = {**trace, "opacity": 0.5, "color": "blue"}
+    c.style.model3d.extra = [dict(backend="plotly", trace=trace_plotly)]
+    return c
+
+
+def create_compound_set(show=False, **kwargs):
+    """creates a styled Collection Compound object with a rotary arrangement of cuboid magnets.
+    A copy is created to show the difference when applying position and/or orientation setters over
+    kwargs."""
+    c1 = make_wheel(name="Magnetic Wheel after")
+    c1.set_children_styles(
+        path_show=False,
+        magnetization_color_north="magenta",
+        magnetization_color_south="cyan",
+    )
+    c2 = make_wheel(name="Magnetic Wheel before")
+    c2.style.model3d.extra[0].trace["color"] = "red"
+    c2.style.model3d.extra[0].trace["opacity"] = 0.1
+    c2.set_children_styles(path_show=False, opacity=0.1)
+    for k, v in kwargs.items():
+        setattr(c1, k, eval(v))
+    if show:
+        fig = go.Figure()
+        magpy.display(c2, c1, path=1, canvas=fig)
+        fig.layout.title = ", ".join(f"c1.{k} = {v}" for k, v in kwargs.items())
+        fig.show()
+    return c1
+
+
+def get_pos_orient_from_collection(coll):
+    """returns a list of (position, orientation.as_matrix()) tuple of a collection and of its
+    children"""
+    pos_orient = []
+    for obj in [coll] + coll.children:
+        pos_orient.append((obj.position, obj.orientation.as_matrix()))
+    return pos_orient
+
+
+def create_compound_test_data(path=None):
+    """creates tests data for compound setters testing"""
+    setters = [
+        ("orientation=None", dict(orientation="None")),
+        ("shorter position path", dict(position="np.array([[50, 0, 100]] * 2)")),
+        (
+            "shorter orientation path",
+            dict(orientation="R.from_rotvec([[90,0,0],[0,90,0]], degrees=True)"),
+        ),
+        (
+            "longer position path",
+            dict(position="np.array(np.linspace((280.,0.,0), (280.,0.,300), 8))"),
+        ),
+        (
+            "longer orientation path",
+            dict(
+                orientation="R.from_rotvec([[0,90*i,0] for i in range(6)], degrees=True)"
+            ),
+        ),
+    ]
+    data = {"test_names": [], "setters_inputs": [], "pos_orient_as_matrix_expected": []}
+    for setter in setters:
+        tname, kwargs = setter
+        coll = create_compound_set(**kwargs)
+        pos_orient = get_pos_orient_from_collection(coll)
+        data["test_names"].append(tname)
+        data["setters_inputs"].append(kwargs)
+        data["pos_orient_as_matrix_expected"].append(pos_orient)
+    if path is None:
+        return data
+    np.save(path, data)
+
+
+def display_compound_test_data(path):
+    """display compound test data from file"""
+    data = np.load(path, allow_pickle=True).item()
+    for kwargs in data["setters_inputs"]:
+        create_compound_set(show=True, **kwargs)
+
+
+folder = "tests/testdata"
+file = os.path.join(folder, "testdata_compound_setter_cases.npy")
+# create_compound_test_data(file)
+
+COMPOUND_DATA = np.load(file, allow_pickle=True).item()
+
+
+@pytest.mark.parametrize(
+    "setters_inputs, pos_orient_as_matrix_expected",
+    [
+        (si, po)
+        for si, po in zip(
+            COMPOUND_DATA["setters_inputs"],
+            COMPOUND_DATA["pos_orient_as_matrix_expected"],
+        )
+    ],
+    ids=COMPOUND_DATA["test_names"],
+)
+def test_compound_setters(setters_inputs, pos_orient_as_matrix_expected):
+    """testing of compound object setters and the effects on its children."""
+    c1 = create_compound_set(**setters_inputs)
+    pos_orient = get_pos_orient_from_collection(c1)
+    for ind, (po, po_exp) in enumerate(zip(pos_orient, pos_orient_as_matrix_expected)):
+        obj_str = "child"
+        if ind == 0:  # first ind is (position, orientation.as_matrix()) of collection
+            obj_str = "Collection"
+        pos, orient = po
+        pos_exp, orient_exp = po_exp
+        err_msg = f"{obj_str} position matching failed"
+        np.testing.assert_almost_equal(pos, pos_exp, err_msg=err_msg)
+        err_msg = f"{obj_str}{ind if ind!=0 else ''} orientation matching failed"
+        np.testing.assert_almost_equal(orient, orient_exp, err_msg=err_msg)
diff --git a/tests/test_CustomSource.py b/tests/test_CustomSource.py
index 67f7825ac..16e63f8a5 100644
--- a/tests/test_CustomSource.py
+++ b/tests/test_CustomSource.py
@@ -4,16 +4,18 @@
 from magpylib._src.exceptions import MagpylibInternalError
 
 # pylint: disable=assignment-from-no-return
-
+# pylint: disable=unused-argument
 
 def constant_Bfield(position=((0, 0, 0))):
+    """ constant field"""
     return np.array([[1, 2, 3]] * len(position))
 
-
 def constant_Hfield(position=((0, 0, 0))):
+    """ constant field - no idea why we need this """
     return np.array([[4, 5, 6]] * len(position))
 
 def bad_Bfield_func(position):
+    """ another constant function without docstring"""
     return np.array([[1, 2, 3]])
 
 def test_CustomSource_basicB():
diff --git a/tests/test_default_utils.py b/tests/test_default_utils.py
index 9a03c61b0..faf16aeb8 100644
--- a/tests/test_default_utils.py
+++ b/tests/test_default_utils.py
@@ -1,6 +1,6 @@
 from copy import deepcopy
 import pytest
-from magpylib._src.default_utils import (
+from magpylib._src.defaults.defaults_utility import (
     MagicProperties,
     color_validator,
     get_defaults_dict,
diff --git a/tests/test_defaults.py b/tests/test_defaults.py
index f1ed599a8..1fc361100 100644
--- a/tests/test_defaults.py
+++ b/tests/test_defaults.py
@@ -1,8 +1,8 @@
 import pytest
 import magpylib as magpy
-from magpylib._src.default_classes import DefaultConfig
+from magpylib._src.defaults.defaults_classes import DefaultConfig
 from magpylib._src.style import DisplayStyle
-from magpylib._src.default_utils import (
+from magpylib._src.defaults.defaults_utility import (
     LINESTYLES_MATPLOTLIB_TO_PLOTLY,
     SYMBOLS_MATPLOTLIB_TO_PLOTLY,
 )
@@ -14,7 +14,7 @@
     "display_autosizefactor": (0,),  # float>0
     "display_animation_maxfps": (0,),  # int>0
     "display_animation_maxframes": (0,),  # int>0
-    "display_animation_slider":  ('notbool'),  # bool
+    "display_animation_slider": ("notbool"),  # bool
     "display_backend": ("plotty",),  # str typo
     "display_colorsequence": (["#2E91E5", "wrongcolor"],),  # iterable of colors
     "display_style_base_path_line_width": (-1,),  # float>=0
@@ -54,6 +54,33 @@
     "display_style_markers_marker_symbol": ("wrongsymbol",),
 }
 
+
+def get_bad_test_data():
+    """create parametrized bad style test data"""
+    bad_test_data = []
+    for k, tup in bad_inputs.items():
+        for v in tup:
+            if "description_text" not in k:
+                if "color" in k and "transition" not in k and "mode" not in k:
+                    # color attributes use a the color validator, which raises a ValueError
+                    errortype = ValueError
+                else:
+                    # all other parameters raise AssertionError
+                    errortype = AssertionError
+            bad_test_data.append((k, v, pytest.raises(errortype)))
+    return bad_test_data
+
+
+@pytest.mark.parametrize(
+    ("key", "value", "expected_errortype"), get_bad_test_data(),
+)
+def test_defaults_bad_inputs(key, value, expected_errortype):
+    """testing defaults setting on bad inputs"""
+    c = DefaultConfig().reset()
+    with expected_errortype:
+        c.update(**{key: value})
+
+
 # dict of good input.
 # This is just for check. dict keys should not be tuples in general, but the test will iterate
 # over the values for each key
@@ -64,7 +91,7 @@
     "display_autosizefactor": (1,),  # float>0
     "display_animation_maxfps": (10,),  # int>0
     "display_animation_maxframes": (200,),  # int>0
-    "display_animation_slider":  (True, False),  # bool
+    "display_animation_slider": (True, False),  # bool
     "display_backend": ("matplotlib", "plotly"),  # str typo
     "display_colorsequence": (
         ["#2E91E5", "#0D2A63"],
@@ -78,11 +105,7 @@
     "display_style_base_path_marker_color": ("blue", "#2E91E5"),  # color
     "display_style_base_path_show": (True, False),  # bool
     "display_style_base_description_show": (True, False),  # bool
-    "display_style_base_description_text": (
-        True,
-        object,
-        "a string",
-    ),  # DOES NOT RAISE, transforms everything into str
+    "display_style_base_description_text": ("a string",),  # string
     "display_style_base_opacity": (0, 0.5, 1),  # 0<=float<=1
     "display_style_base_model3d_show": (True, False),
     "display_style_base_color": ("blue", "#2E91E5"),  # color
@@ -105,56 +128,36 @@
     "display_style_sensor_pixel_color": ("blue", "#2E91E5"),
     "display_style_sensor_pixel_symbol": SYMBOLS_MATPLOTLIB_TO_PLOTLY.keys(),
     "display_style_dipole_size": (0, 1),  # float>=0
-    "display_style_dipole_pivot": (
-        "middle",
-        "tail",
-        "tip",
-    ),  # pivot middle, tail, tip
+    "display_style_dipole_pivot": ("middle", "tail", "tip",),  # pivot middle, tail, tip
     "display_style_markers_marker_size": (0, 1),  # float>=0
     "display_style_markers_marker_color": ("blue", "#2E91E5"),
     "display_style_markers_marker_symbol": SYMBOLS_MATPLOTLIB_TO_PLOTLY.keys(),
 }
 
 
-def test_defaults():
-    """test setting and resetting the config"""
-    magpy.defaults.itercylinder = 15
-    assert magpy.defaults.itercylinder == 15, "setting config failed"
-    magpy.defaults.reset()
-    assert magpy.defaults.itercylinder == 50, "resetting config failed"
-
-
-def test_defaults_bad_inputs():
-    """testing defaults setting on bad inputs"""
-    c = DefaultConfig().reset()
-    for k, tup in bad_inputs.items():
-        for v in tup:
-            if "description_text" not in k:
-                if "color" in k and "transition" not in k and "mode" not in k:
-                    # color attributes use a the color validator, which raises a ValueError
-                    errortype = ValueError
-                else:
-                    # all other parameters raise AssertionError
-                    errortype = AssertionError
-                with pytest.raises(errortype):
-                    c.update(**{k: v})
+def get_good_test_data():
+    """create parametrized good style test data"""
+    good_test_data = []
+    for key, tup in good_inputs.items():
+        for value in tup:
+            expected = value
+            if "color" in key and isinstance(value, str):
+                expected = value.lower()  # hex color gets lowered
+            good_test_data.append((key, value, expected))
+    return good_test_data
 
 
-def test_defaults_good_inputs():
+@pytest.mark.parametrize(
+    ("key", "value", "expected"), get_good_test_data(),
+)
+def test_defaults_good_inputs(key, value, expected):
     """testing defaults setting on bad inputs"""
     c = DefaultConfig()
-    for k, tup in good_inputs.items():
-        for v1 in tup:
-            c.update(**{k: v1})
-            v0 = c
-            for v in k.split("_"):
-                v0 = getattr(v0, v)
-            if "color" in k and isinstance(v1, str):
-                v1 = v1.lower()  # hex color gets lowered
-            elif "description_text" in k:
-                # for a desc text, any object is valid and is transformed into a string
-                v1 = str(v1)
-            assert v0 == v1, f"{k} should be {v1}, but received {v0} instead"
+    c.update(**{key: value})
+    v0 = c
+    for v in key.split("_"):
+        v0 = getattr(v0, v)
+    assert v0 == expected, f"{key} should be {expected}, but received {v0} instead"
 
 
 def test_bad_input_classes():
@@ -187,3 +190,11 @@ def test_bad_input_classes():
     for s in style_classes:
         with pytest.raises(ValueError):
             c.update(**{s: "bad class"})
+
+
+def test_resetting_defaults():
+    """test setting and resetting the config"""
+    magpy.defaults.itercylinder = 15
+    assert magpy.defaults.itercylinder == 15, "setting config failed"
+    magpy.defaults.reset()
+    assert magpy.defaults.itercylinder == 50, "resetting config failed"
diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py
index afca00968..da4ef5e79 100644
--- a/tests/test_display_matplotlib.py
+++ b/tests/test_display_matplotlib.py
@@ -3,7 +3,7 @@
 import matplotlib.pyplot as plt
 import magpylib as magpy
 from magpylib.magnet import Cylinder, Cuboid, Sphere, CylinderSegment
-from magpylib._src.display.plotly_draw import make_BaseCuboid
+from magpylib._src.display.plotly.plotly_base_traces import make_BaseCuboid
 
 # pylint: disable=assignment-from-no-return
 
@@ -14,8 +14,7 @@ def test_Cylinder_display():
     src = Cylinder((1, 2, 3), (1, 2))
     x = src.display(canvas=ax, path=15)
     assert x is None, "path should revert to True"
-    path_len = 30
-    src.move([(0.4, 0.4, 0.4)] * path_len, increment=True)
+    src.move(np.linspace((.4,.4,.4), (12,12,12), 30), start=-1)
     x = src.display(canvas=ax, path=False, show_direction=True)
     assert x is None, "display test fail"
 
@@ -33,7 +32,7 @@ def test_CylinderSegment_display():
     x = src.display(canvas=ax, path=15)
     assert x is None, "path should revert to True"
 
-    src.move([(0.4, 0.4, 0.4)] * 33, increment=True)
+    src.move(np.linspace((.4,.4,.4), (13.2,13.2,13.2), 33), start=-1)
     x = src.display(canvas=ax, path=False, show_direction=True)
     assert x is None, "display test fail"
 
@@ -45,7 +44,7 @@ def test_Sphere_display():
     x = src.display(canvas=ax, path=15)
     assert x is None, "path should revert to True"
 
-    src.move([(0.4, 0.4, 0.4)] * 20, increment=True)
+    src.move(np.linspace((.4,.4,.4), (8,8,8), 20), start=-1)
     x = src.display(canvas=ax, path=False, show_direction=True)
     assert x is None, "display test fail"
 
@@ -53,7 +52,7 @@ def test_Sphere_display():
 def test_Cuboid_display():
     """testing display"""
     src = Cuboid((1, 2, 3), (1, 2, 3))
-    src.move([(0.1, 0.1, 0.1)] * 20, increment=True)
+    src.move(np.linspace((.1,.1,.1), (2,2,2), 20), start=-1)
     plt.ion()
     x = src.display(path=5, show_direction=True)
     plt.close()
@@ -68,7 +67,8 @@ def test_Sensor_display():
     """testing display"""
     ax = plt.subplot(projection="3d")
     sens = magpy.Sensor(pixel=[(1, 2, 3), (2, 3, 4)])
-    sens.move([(0.4, 0.4, 0.4)] * 33, increment=True)
+    poz = np.linspace((.4,.4,.4), (13.2,13.2,13.2), 33)
+    sens.move(poz, start=-1)
     x = sens.display(canvas=ax, markers=[(100, 100, 100)], path=15)
     assert x is None, "display test fail"
 
@@ -113,7 +113,7 @@ def test_dipole_display():
     ax2 = plt.subplot(projection="3d")
     dip = magpy.misc.Dipole(moment=(1, 2, 3), position=(2, 2, 2))
     dip2 = magpy.misc.Dipole(moment=(1, 2, 3), position=(2, 2, 2))
-    dip2.move([(0.4, 0.4, 0.4)] * 5, increment=True)
+    dip2.move(np.linspace((.4,.4,.4), (2,2,2), 5), start=-1)
     x = dip.display(canvas=ax2)
     assert x is None, "display test fail"
     x = dip.display(canvas=ax2, path=2)
@@ -126,10 +126,10 @@ def test_circular_line_display():
     ax2 = plt.subplot(projection="3d")
     src1 = magpy.current.Loop(1, 2)
     src2 = magpy.current.Loop(1, 2)
-    src1.move([(0.4, 0.4, 0.4)] * 5, increment=True)
+    src1.move(np.linspace((.4,.4,.4), (2,2,2), 5), start=-1)
     src3 = magpy.current.Line(1, [(0, 0, 0), (1, 1, 1), (2, 2, 2)])
     src4 = magpy.current.Line(1, [(0, 0, 0), (1, 1, 1), (2, 2, 2)])
-    src3.move([(0.4, 0.4, 0.4)] * 5, increment=False)
+    src3.move([(.4,.4,.4)]*5, start=-1)
     x = src1.display(canvas=ax2, path=2, style_arrow_size=0)
     assert x is None, "display test fail"
     x = src2.display(canvas=ax2)
@@ -144,7 +144,7 @@ def test_matplotlib_animation_warning():
     """animate with matplotlib should raise UserWarning"""
     ax = plt.subplot(projection="3d")
     sens = magpy.Sensor(pixel=[(1, 2, 3), (2, 3, 4)])
-    sens.move([(0.4, 0.4, 0.4)] * 33, increment=True)
+    sens.move(np.linspace((.4,.4,.4), (12.4,12.4,12.4), 33), start=-1)
     with pytest.warns(UserWarning):
         sens.display(canvas=ax, path="animate")
 
@@ -153,7 +153,7 @@ def test_matplotlib_model3d_extra():
     """test display extra model3d"""
     cuboid = magpy.magnet.Cuboid(
         magnetization=(1, 0, 0), dimension=(3, 3, 3), position=(10, 0, 0)
-    ).rotate_from_angax([72] * 5, "z", anchor=(0, 0, 0), start=0, increment=True)
+    ).rotate_from_angax(np.linspace(72,360,5), "z", anchor=(0, 0, 0), start=0)
     cuboid.style.model3d.show = False
     ax = plt.subplot(projection="3d")
     cuboid.style.model3d.extra = [
diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py
index 61bdb5d73..1ee8e7223 100644
--- a/tests/test_display_plotly.py
+++ b/tests/test_display_plotly.py
@@ -1,8 +1,9 @@
+import numpy as np
 import plotly.graph_objects as go
 import pytest
 import magpylib as magpy
 from magpylib.magnet import Cylinder, Cuboid, Sphere, CylinderSegment
-from magpylib._src.display.plotly_draw import get_plotly_traces
+from magpylib._src.display.plotly.plotly_display import get_plotly_traces
 
 # pylint: disable=assignment-from-no-return
 
@@ -15,7 +16,7 @@ def test_Cylinder_display():
     x = src.display(canvas=fig, path=15)
     assert x is None, "path should revert to True"
 
-    src.move([(0.4, 0.4, 0.4)] * 33, increment=True)
+    src.move(np.linspace((.4,.4,.4), (12.4,12.4,12.4), 33), start=-1)
     x = src.display(
         canvas=fig,
         path=False,
@@ -33,7 +34,7 @@ def test_CylinderSegment_display():
     x = src.display(canvas=fig, path=15)
     assert x is None, "path should revert to True"
 
-    src.move([(0.4, 0.4, 0.4)] * 33, increment=True)
+    src.move(np.linspace((.4,.4,.4), (12.4,12.4,12.4), 33), start=-1)
     x = src.display(
         canvas=fig,
         path=False,
@@ -51,7 +52,7 @@ def test_Sphere_display():
     x = src.display(canvas=fig, path=15)
     assert x is None, "path should revert to True"
 
-    src.move([(0.4, 0.4, 0.4)] * 20, increment=True)
+    src.move(np.linspace((.4,.4,.4), (8,8,8), 33), start=-1)
     x = src.display(canvas=fig, path=False, style_magnetization_show=True)
     assert x is None, "display test fail"
 
@@ -60,7 +61,7 @@ def test_Cuboid_display():
     """testing display"""
     magpy.defaults.display.backend = "plotly"
     src = Cuboid((1, 2, 3), (1, 2, 3))
-    src.move([(0.1, 0.1, 0.1)] * 20, increment=True)
+    src.move(np.linspace((.1,.1,.1), (2,2,2), 20), start=-1)
     x = src.display(path=5, style_magnetization_show=True, renderer="json")
     assert x is None, "display test fail"
 
@@ -77,7 +78,7 @@ def test_Sensor_display():
     x = sens_nopix.display(canvas=fig, style_description_text="mysensor")
     assert x is None, "display test fail"
     sens = magpy.Sensor(pixel=[(1, 2, 3), (2, 3, 4)])
-    sens.move([(0.4, 0.4, 0.4)] * 33, increment=True)
+    sens.move(np.linspace((.4,.4,.4), (12.4,12.4,12.4), 33), start=-1)
     x = sens.display(canvas=fig, markers=[(100, 100, 100)], path=15)
     assert x is None, "display test fail"
     x = sens.display(canvas=fig, markers=[(100, 100, 100)], path=False)
@@ -117,7 +118,7 @@ def test_dipole_display():
     dip1 = magpy.misc.Dipole(moment=(1, 2, 3), position=(1, 1, 1))
     dip2 = magpy.misc.Dipole(moment=(1, 2, 3), position=(2, 2, 2))
     dip3 = magpy.misc.Dipole(moment=(1, 2, 3), position=(3, 3, 3))
-    dip2.move([(0.4, 0.4, 0.4)] * 5, increment=True)
+    dip2.move(np.linspace((.4,.4,.4), (2,2,2), 5), start=-1)
     x = dip1.display(canvas=fig, style_pivot="tail")
     assert x is None, "display test fail"
     x = dip2.display(canvas=fig, path=2, style_pivot="tip")
@@ -133,10 +134,10 @@ def test_circular_line_display():
     fig = go.Figure()
     src1 = magpy.current.Loop(1, 2)
     src2 = magpy.current.Loop(1, 2)
-    src1.move([(0.4, 0.4, 0.4)] * 5, increment=True)
+    src1.move(np.linspace((.4,.4,.4), (2,2,2), 5), start=-1)
     src3 = magpy.current.Line(1, [(0, 0, 0), (1, 1, 1), (2, 2, 2)])
     src4 = magpy.current.Line(1, [(0, 0, 0), (1, 1, 1), (2, 2, 2)])
-    src3.move([(0.4, 0.4, 0.4)] * 5, increment=False)
+    src3.move([(0.4, 0.4, 0.4)] * 5, start=-1)
     x = src1.display(canvas=fig, path=2, style_arrow_show=False)
     assert x is None, "display test fail"
     x = src2.display(canvas=fig)
@@ -190,7 +191,8 @@ class Unkwnown2DPosition:
 def test_extra_model3d():
     """test diplay when object has an extra model object attached"""
     magpy.defaults.display.backend = "plotly"
-    cuboid = Cuboid((1, 2, 3), (1, 2, 3)).move([(0.4, 0.4, 0.4)] * 33, increment=True)
+    cuboid = Cuboid((1, 2, 3), (1, 2, 3))
+    cuboid.move(np.linspace((.4,.4,.4), (12.4,12.4,12.4), 33), start=-1)
     cuboid.style.model3d.extra = [
         {
             "backend": "plotly",
@@ -223,7 +225,9 @@ def test_extra_model3d():
     cuboid.style.model3d.extra[0].show = False
     x = cuboid.display(canvas=fig)
     assert x is None, "display test fail"
-    x = cuboid.display(
+    coll = magpy.Collection(cuboid)
+    coll.rotate_from_angax(45, 'z')
+    x = magpy.display(coll,
         canvas=fig,
         path="animate",
         style=dict(model3d_show=False),
@@ -269,7 +273,8 @@ def test_display_warnings():
     magpy.defaults.display.backend = "plotly"
     magpy.defaults.display.animation.maxfps = 2
     magpy.defaults.display.animation.maxframes = 2
-    src = Cuboid((1, 2, 3), (1, 2, 3)).move([(0.4, 0.4, 0.4)] * 10, increment=True)
+    src = Cuboid((1, 2, 3), (1, 2, 3))
+    src.move(np.linspace((.4,.4,.4), (4,4,4), 10), start=-1)
     fig = go.Figure()
 
     with pytest.warns(UserWarning):  # animate_fps to big warning
diff --git a/tests/test_display_utility.py b/tests/test_display_utility.py
index 214686dcf..3f3c61c68 100644
--- a/tests/test_display_utility.py
+++ b/tests/test_display_utility.py
@@ -1,5 +1,5 @@
 import numpy as np
-from magpylib._src.display.disp_utility import draw_arrow_from_vertices
+from magpylib._src.display.display_utility import draw_arrow_from_vertices
 
 def test_draw_arrow_from_vertices():
     """tests also the edge case when a vertex is in -y direction"""
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 1b624bb4d..6a7be6c1e 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -159,7 +159,7 @@ def utility_test_path_format():
     """ bad path format input
     """
     pm1 = magpy.magnet.Cuboid((1,2,3),(1,2,3))
-    pm1.position = [(1,2,3),(1,2,3)]
+    pm1._position = [(1,2,3),(1,2,3)]
     tpf(pm1)
 
 
diff --git a/tests/test_field_functions.py b/tests/test_field_functions.py
index fb26caa38..c54ac1b82 100644
--- a/tests/test_field_functions.py
+++ b/tests/test_field_functions.py
@@ -7,7 +7,7 @@
 from magpylib._src.fields.field_BH_dipole import field_BH_dipole
 from magpylib._src.fields.field_BH_loop import field_BH_loop
 from magpylib._src.fields.field_BH_line import field_BH_line, field_BH_line_from_vert
-from magpylib._src.default_classes import default_settings as Config
+from magpylib._src.defaults.defaults_classes import default_settings as Config
 
 # # GENERATE TEST DATA
 # n = 500
diff --git a/tests/test_getBH_dict.py b/tests/test_getBH_dict.py
index bd503feb4..8379a7195 100644
--- a/tests/test_getBH_dict.py
+++ b/tests/test_getBH_dict.py
@@ -13,9 +13,9 @@ def test_getB_dict1():
     dim = [3,3]
 
     pm = Cylinder(mag, dim)
-    pm.move([(.5,0,0)]*15, increment=True)
+    pm.move(np.linspace((.5,0,0), (7.5,0,0), 15), start=-1)
     pm.rotate_from_angax(np.linspace(0,666,25), 'y', anchor=0)
-    pm.move([(0,x,0) for x in np.linspace(0,5,5)])
+    pm.move([(0,x,0) for x in np.linspace(0,5,5)], start=-1)
     B2 = pm.getB(pos_obs)
 
     pos = pm.position
diff --git a/tests/test_getBH_interfaces.py b/tests/test_getBH_interfaces.py
index a52ec7bbb..f288993ba 100644
--- a/tests/test_getBH_interfaces.py
+++ b/tests/test_getBH_interfaces.py
@@ -6,7 +6,7 @@ def test_getB_interfaces1():
     """ self-consitent test of different possibilities for computing the field
     """
     src = magpy.magnet.Cuboid((1,2,3), (1,2,3))
-    src.move([(.1,.2,.3)]*10, increment=True)
+    src.move(np.linspace((.1,.2,.3), (1,2,3), 10), start=-1)
     poso = [[(-1,-1,-1)]*2]*2
     sens = magpy.Sensor(pixel=poso)
     B = magpy.getB_dict(
@@ -39,7 +39,7 @@ def test_getB_interfaces2():
     """ self-consitent test of different possibilities for computing the field
     """
     src = magpy.magnet.Cuboid((1,2,3), (1,2,3))
-    src.move([(.1,.2,.3)]*10, increment=True)
+    src.move(np.linspace((.1,.2,.3), (1,2,3), 10), start=-1)
     poso = [[(-1,-1,-1)]*2]*2
     sens = magpy.Sensor(pixel=poso)
     B = magpy.getB_dict(
@@ -65,7 +65,7 @@ def test_getB_interfaces3():
     """ self-consitent test of different possibilities for computing the field
     """
     src = magpy.magnet.Cuboid((1,2,3), (1,2,3))
-    src.move([(.1,.2,.3)]*10, increment=True)
+    src.move(np.linspace((.1,.2,.3), (1,2,3), 10), start=-1)
     poso = [[(-1,-1,-1)]*2]*2
     sens = magpy.Sensor(pixel=poso)
     B = magpy.getB_dict(
@@ -97,7 +97,7 @@ def test_getH_interfaces1():
     mag=(22,-33,44)
     dim=(3,2,3)
     src = magpy.magnet.Cuboid(mag, dim)
-    src.move([(.1,.2,.3)]*10, increment=True)
+    src.move(np.linspace((.1,.2,.3), (1,2,3), 10), start=-1)
 
     poso = [[(-1,-2,-3)]*2]*2
     sens = magpy.Sensor(pixel=poso)
@@ -134,7 +134,7 @@ def test_getH_interfaces2():
     mag=(22,-33,44)
     dim=(3,2,3)
     src = magpy.magnet.Cuboid(mag, dim)
-    src.move([(.1,.2,.3)]*10, increment=True)
+    src.move(np.linspace((.1,.2,.3), (1,2,3), 10), start=-1)
 
     poso = [[(-1,-2,-3)]*2]*2
     sens = magpy.Sensor(pixel=poso)
@@ -164,7 +164,7 @@ def test_getH_interfaces3():
     mag=(22,-33,44)
     dim=(3,2,3)
     src = magpy.magnet.Cuboid(mag, dim)
-    src.move([(.1,.2,.3)]*10, increment=True)
+    src.move(np.linspace((.1,.2,.3), (1,2,3), 10), start=-1)
 
     poso = [[(-1,-2,-3)]*2]*2
     sens = magpy.Sensor(pixel=poso)
diff --git a/tests/test_getBH_level2.py b/tests/test_getBH_level2.py
index 58b0ff757..b122a5e7c 100644
--- a/tests/test_getBH_level2.py
+++ b/tests/test_getBH_level2.py
@@ -116,7 +116,8 @@ def test_getB_level2_input_path():
 
     fb = pm1.getB([(x,0,0) for x in np.linspace(0,-1,11)])
 
-    pm1.move([(.1,0,0)]*10, start='append', increment=True)
+    possis = np.linspace((.1,0,0), (1,0,0), 10)
+    pm1.move(possis)
     B=magpy.getB(pm1,(0,0,0))
     result = fb
     assert B.shape == result.shape, "FAILOR3a shape"
@@ -151,7 +152,8 @@ def test_path_tile():
     """
     pm1 = magpy.magnet.Cuboid((11,22,33),(1,2,3))
     pm2 = magpy.magnet.Cuboid((11,22,33),(1,2,3))
-    pm2.move([[10/33]*3]*33, increment=True)
+    poz = np.linspace((10/33,10/33,10/33), (10,10,10), 33)
+    pm2.move(poz)
 
     path1p = pm1.position
     path1r = pm1.orientation
@@ -172,7 +174,7 @@ def test_sensor_rotation1():
     """
     src = magpy.magnet.Cuboid((1000,0,0),(1,1,1))
     sens = magpy.Sensor(position=(1,0,0))
-    sens.rotate_from_angax([360/55]*55, 'z', start=1, anchor=None, increment=True)
+    sens.rotate_from_angax(np.linspace(0,360,56)[1:], 'z', start=1, anchor=None)
     B = src.getB(sens)
 
     B0 = B[0,0]
@@ -190,7 +192,7 @@ def test_sensor_rotation2():
 
     poss = (0,0,0)
     sens = magpy.Sensor(pixel=poss)
-    sens.rotate_from_angax([0,45,90], 'z')
+    sens.rotate_from_angax([45,90], 'z')
 
     sens2 = magpy.Sensor(pixel=poss)
     sens2.rotate_from_angax(-45,'z')
@@ -229,7 +231,7 @@ def test_sensor_rotation3():
     B0 = magpy.getB(src,sens)
     B0t = np.tile(B0,(12,1))
 
-    sens.move([(0,0,0)]*12)
+    sens.move([(0,0,0)]*12, start=-1)
     Bpath = magpy.getB(src,sens)
 
     assert np.allclose(B0t,Bpath)
@@ -239,17 +241,17 @@ def test_object_tiling():
     """ test if object tiling works when input paths are of various lengths
     """
     src1 = magpy.current.Loop(current=1, diameter=1)
-    src1.rotate_from_angax([1]*31, 'x', anchor=(0,1,0), increment=True)
+    src1.rotate_from_angax(np.linspace(1,31,31), 'x', anchor=(0,1,0), start=-1)
 
     src2 = magpy.magnet.Cuboid(magnetization=(1,1,1), dimension=(1,1,1), position=(1,1,1))
-    src2.move([(1,1,1)]*21)
+    src2.move([(1,1,1)]*21, start=-1)
 
     src3 = magpy.magnet.Cuboid(magnetization=(1,1,1), dimension=(1,1,1), position=(1,1,1))
     src4 = magpy.magnet.Cuboid(magnetization=(1,1,1), dimension=(1,1,1), position=(1,1,1))
 
     col = magpy.Collection(src3, src4)
-    src3.move([(1,1,1)]*12)
-    src4.move([(1,1,1)]*31)
+    src3.move([(1,1,1)]*12, start=-1)
+    src4.move([(1,1,1)]*31, start=-1)
 
     possis = [[1,2,3]]*5
     sens = magpy.Sensor(pixel=possis)
diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py
index b94f885c2..829b38517 100644
--- a/tests/test_input_checks.py
+++ b/tests/test_input_checks.py
@@ -96,16 +96,11 @@ def badInput_move2():
     bg.move([1,2,3], start=1.1)
 
 def badInput_move3():
-    """ bad increment input type"""
-    bg = BaseGeo(a3, r0)
-    bg.move([1,2,3], increment=5)
-
-def badInput_move4():
     """ bad displacement input format1"""
     bg = BaseGeo(a3, r0)
     bg.move([1,2])
 
-def badInput_move5():
+def badInput_move4():
     """ bad displacement input format2"""
     bg = BaseGeo(a3, r0)
     bg.move([[[1,2,3]]*3]*4)
@@ -127,11 +122,6 @@ def badInput_rotate3():
     bg = BaseGeo(a3, r0)
     bg.rotate(r0, start=1.23)
 
-def badInput_rotate4():
-    """ bad increment input type"""
-    bg = BaseGeo(a3, r0)
-    bg.rotate(r0, increment=1)
-
 def badInput_rotate5():
     """ bad anchor input format"""
     bg = BaseGeo(a3, r0)
@@ -159,11 +149,6 @@ def badInput_rotate_from_angax4():
     bg = BaseGeo(a3, r0)
     bg.rotate_from_angax(123, (1,2,3), start=1.1)
 
-def badInput_rotate_from_angax5():
-    """ bad increment input type"""
-    bg = BaseGeo(a3, r0)
-    bg.rotate_from_angax(123, (1,2,3), increment=None)
-
 def badInput_rotate_from_angax6():
     """ bad degrees input type"""
     bg = BaseGeo(a3, r0)
@@ -184,6 +169,11 @@ def badInput_rotate_from_angax9():
     bg = BaseGeo(a3, r0)
     bg.rotate_from_angax(123, [0,0,0])
 
+def badInput_rotate_from_angax10():
+    """ bad axis input format"""
+    bg = BaseGeo(a3, r0)
+    bg.rotate_from_angax(123, [1,2,3], anchor=[[(1,2,3)]*2]*2)
+
 
 # EXCITATIONS -----------------------------------------
 
@@ -305,16 +295,14 @@ def test_move(self):
         """ bad .move inputs"""
         self.assertRaises(MagpylibBadUserInput, badInput_move1)
         self.assertRaises(MagpylibBadUserInput, badInput_move2)
-        self.assertRaises(MagpylibBadUserInput, badInput_move3)
+        self.assertRaises(MagpylibBadInputShape, badInput_move3)
         self.assertRaises(MagpylibBadInputShape, badInput_move4)
-        self.assertRaises(MagpylibBadInputShape, badInput_move5)
 
     def test_rotate(self):
         """ bad .rotate inputs"""
         self.assertRaises(MagpylibBadUserInput, badInput_rotate1)
         self.assertRaises(MagpylibBadUserInput, badInput_rotate2)
         self.assertRaises(MagpylibBadUserInput, badInput_rotate3)
-        self.assertRaises(MagpylibBadUserInput, badInput_rotate4)
         self.assertRaises(MagpylibBadInputShape, badInput_rotate5)
 
     def test_rotate_from_angax(self):
@@ -323,11 +311,11 @@ def test_rotate_from_angax(self):
         self.assertRaises(MagpylibBadUserInput, badInput_rotate_from_angax2)
         self.assertRaises(MagpylibBadUserInput, badInput_rotate_from_angax3)
         self.assertRaises(MagpylibBadUserInput, badInput_rotate_from_angax4)
-        self.assertRaises(MagpylibBadUserInput, badInput_rotate_from_angax5)
         self.assertRaises(MagpylibBadUserInput, badInput_rotate_from_angax6)
         self.assertRaises(MagpylibBadInputShape, badInput_rotate_from_angax7)
         self.assertRaises(MagpylibBadInputShape, badInput_rotate_from_angax8)
         self.assertRaises(MagpylibBadUserInput, badInput_rotate_from_angax9)
+        self.assertRaises(MagpylibBadInputShape, badInput_rotate_from_angax10)
 
     def test_magnetization_input(self):
         """ bad magnetization inputs"""
diff --git a/tests/test_numerical_stability.py b/tests/test_numerical_stability.py
index 63671f52b..9cccddc87 100644
--- a/tests/test_numerical_stability.py
+++ b/tests/test_numerical_stability.py
@@ -15,7 +15,7 @@ def test_loop_field():
     anch = (0,0,1)
     B = []
     for _ in range(1000):
-        lop.rotate_from_angax(100, 'x', anchor=anch)
+        lop.rotate_from_angax(100, 'x', anchor=anch, start=-1)
         B += [lop.getB(anch)]
 
     B = np.array(B)
diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py
index a8a1555de..ddf31e8b1 100644
--- a/tests/test_obj_BaseGeo.py
+++ b/tests/test_obj_BaseGeo.py
@@ -1,5 +1,3 @@
-# import os
-# import pickle
 import numpy as np
 import pytest
 from scipy.spatial.transform import Rotation as R
@@ -9,6 +7,7 @@
 
 def test_BaseGeo_basics():
     """fundamental usage test"""
+    # pylint: disable=no-member
 
     ptest = np.array(
         [
@@ -50,7 +49,7 @@ def test_BaseGeo_basics():
     rots += [bgeo.orientation.as_rotvec()]
 
     rot = R.from_rotvec((-0.1, -0.2, -0.3))
-    bgeo.rotate(rotation=rot)
+    bgeo.rotate(rotation=rot, start=-1)
     poss += [bgeo.position.copy()]
     rots += [bgeo.orientation.as_rotvec()]
 
@@ -63,7 +62,8 @@ def test_BaseGeo_basics():
     rots += [bgeo.orientation.as_rotvec()]
 
     rot = R.from_rotvec((0.1, 0.2, 0.3))
-    bgeo.rotate(rot, anchor=(3, 2, 1)).rotate_from_angax(33, (3, 2, 1), anchor=0)
+    bgeo.rotate(rot, anchor=(3, 2, 1), start=-1)
+    bgeo.rotate_from_angax(33, (3, 2, 1), anchor=0, start=-1)
     poss += [bgeo.position.copy()]
     rots += [bgeo.orientation.as_rotvec()]
 
@@ -84,10 +84,11 @@ def test_rotate_vs_rotate_from():
         (0, 0, 0.4),
         (0, -0.2, 0),
     ]
-    rroz = R.from_rotvec(roz)
 
     bg1 = BaseGeo(position=(3, 4, 5), orientation=R.from_quat((0, 0, 0, 1)))
-    bg1.rotate(rotation=rroz, anchor=(-3, -2, 1), start=1, increment=True)
+    for ro in roz:
+        rroz = R.from_rotvec((ro,))
+        bg1.rotate(rotation=rroz, anchor=(-3, -2, 1))
     pos1 = bg1.position
     ori1 = bg1.orientation.as_quat()
 
@@ -95,8 +96,7 @@ def test_rotate_vs_rotate_from():
     angs = np.linalg.norm(roz, axis=1)
     for ang, ax in zip(angs, roz):
         bg2.rotate_from_angax(
-            angle=ang, degrees=False, axis=ax, anchor=(-3, -2, 1), start="append"
-        )
+            angle=[ang], degrees=False, axis=ax, anchor=(-3, -2, 1))
     pos2 = bg2.position
     ori2 = bg2.orientation.as_quat()
 
@@ -110,7 +110,7 @@ def test_BaseGeo_reset_path():
     bg = BaseGeo((0, 0, 0), R.from_quat((0, 0, 0, 1)))
     bg.move([(1, 1, 1)] * 11)
 
-    assert len(bg._position) == 11, "bad path generation"
+    assert len(bg._position) == 12, "bad path generation"
 
     bg.reset_path()
     assert len(bg._position) == 1, "bad path reset"
@@ -120,7 +120,7 @@ def test_BaseGeo_anchor_None():
     """testing rotation with None anchor"""
     pos = np.array([1, 2, 3])
     bg = BaseGeo(pos, R.from_quat((0, 0, 0, 1)))
-    bg.rotate(R.from_rotvec([(0, 0, 0), (0.1, 0.2, 0.3), (0.2, 0.4, 0.6)]))
+    bg.rotate(R.from_rotvec([(0.1, 0.2, 0.3), (0.2, 0.4, 0.6)]))
 
     pos3 = np.array([pos] * 3)
     rot3 = np.array([(0, 0, 0), (0.1, 0.2, 0.3), (0.2, 0.4, 0.6)])
@@ -143,12 +143,12 @@ def test_attach():
     """test attach functionality"""
     bg = BaseGeo([0, 0, 0], R.from_rotvec((0, 0, 0)))
     rot_obj = R.from_rotvec([(x, 0, 0) for x in np.linspace(0, 10, 11)])
-    bg.rotate(rot_obj)
+    bg.rotate(rot_obj, start=-1)
 
     bg2 = BaseGeo([0, 0, 0], R.from_rotvec((0, 0, 0)))
-    roto = R.from_rotvec((1, 0, 0))
+    roto = R.from_rotvec(((1, 0, 0),))
     for _ in range(10):
-        bg2.rotate(roto, start="append")
+        bg2.rotate(roto)
 
     assert np.allclose(bg.position, bg2.position), "attach p"
     assert np.allclose(bg.orientation.as_quat(), bg2.orientation.as_quat()), "attach o"
@@ -225,13 +225,13 @@ def test_path_functionality2():
     assert np.allclose(pos, P)
     assert np.allclose(ori, Q)
 
-    pos, ori = evall(BaseGeo(pos0, rot0).move(inpath, start=6))
+    pos, ori = evall(BaseGeo(pos0, rot0).move(inpath, start=5))
     P = np.array([b1, b2, b3, b4, b5, b5 + c1, b5 + c2, b5 + c3])
     Q = np.array([q1, q2, q3, q4, q5, q5, q5, q5])
     assert np.allclose(pos, P)
     assert np.allclose(ori, Q)
 
-    pos, ori = evall(BaseGeo(pos0, rot0).move(inpath, start="append"))
+    pos, ori = evall(BaseGeo(pos0, rot0).move(inpath))
     P = np.array([b1, b2, b3, b4, b5, b5 + c1, b5 + c2, b5 + c3])
     Q = np.array([q1, q2, q3, q4, q5, q5, q5, q5])
     assert np.allclose(pos, P)
@@ -271,14 +271,9 @@ def test_path_functionality3():
     assert np.allclose(pos1, pos2)
     assert np.allclose(ori1, ori2)
 
-    pos1, ori1 = evall(BaseGeo(pos0, rot0).move(inpath, start=-6))
-    pos2, ori2 = evall(BaseGeo(pos0, rot0).move(inpath, start=-5))
-    assert np.allclose(pos1, pos2)
-    assert np.allclose(ori1, ori2)
-
 
 def test_scipy_from_methods():
-    """test all rotation methods inspired from scipy implemented in BaseRotation"""
+    """test all rotation methods inspired from scipy implemented in BaseTransform"""
     cube = lambda: magpy.magnet.Cuboid((11, 22, 33), (1, 1, 1))
     angs_deg = np.linspace(0, 360, 10)
     angs = np.deg2rad(angs_deg)
diff --git a/tests/test_obj_BaseGeo_v4motion.py b/tests/test_obj_BaseGeo_v4motion.py
new file mode 100644
index 000000000..cbf0fe96b
--- /dev/null
+++ b/tests/test_obj_BaseGeo_v4motion.py
@@ -0,0 +1,346 @@
+import numpy as np
+import pytest
+from scipy.spatial.transform import Rotation as R
+import magpylib as magpy
+
+###############################################################################
+###############################################################################
+# NEW BASE GEO TESTS FROM v4
+
+def validate_pos_orient(obj, ppath, opath_as_rotvec):
+    """ test position (ppath) and orientation (opath) of BaseGeo object (obj) """
+    sp = obj.position
+    so = obj.orientation
+    ppath = np.array(ppath)
+    opath = R.from_rotvec(opath_as_rotvec)
+    assert ppath.shape == sp.shape, (
+        "position shapes do not match"
+        f"\n object has {sp.shape} instead of {ppath.shape}"
+    )
+    assert opath.as_rotvec().shape == so.as_rotvec().shape, (
+        "orientation as_rotvec shapes do not match"
+        f"\n object has {so.as_rotvec().shape} instead of {opath.as_rotvec().shape}"
+    )
+    assert np.allclose(sp, ppath), (
+        f"position validation failed with ({sp})" f"\n expected {ppath}"
+    )
+    assert np.allclose(so.as_matrix(), opath.as_matrix()), (
+        f"orientation validation failed with ({so.as_rotvec()})"
+        f"\n expected {opath_as_rotvec}"
+    )
+
+
+###############################################################################
+###############################################################################
+# BASEGEO POS/ORI INIT TESTING
+# at initialization position and orientation must have similar shape (N,3)
+# - if inputs are (N,3) and (3,) then the (3,) is tiled up to (N,3)
+# - if inputs are (N,3) and (M,3) then the smaller one is padded up
+# - None orientation input is interpreted as (0,0,0) rotvec == (0,0,0,1) quat
+
+def get_init_pos_orient_test_data():
+    """
+    returns data for object init testing
+
+    init_position, init_orientation_rotvec, expected_position, expected_orientation_rotvec
+    """
+    p0 = (1, 2, 3)
+    p1 = [(1, 2, 3)]
+    p2 = [(1, 2, 3), (1, 1, 1)]
+    o0 = None
+    o1 = (0, 0, 0.1)
+    o2 = [(0, 0, 0.1)]
+    o3 = [(0, 0, 0.1), (0, 0, 0.2)]
+    o4 = [(0, 0, 0.1), (0, 0, 0.2), (0, 0, 0.3)]
+
+    init_test_data = [
+        [p0, o0, p0, (0, 0, 0)],
+        [p0, o1, p0, o1],
+        [p0, o2, p0, o1],
+        [p0, o3, (p0, p0), o3],
+        [p1, o0, p0, (0, 0, 0)],
+        [p1, o1, p0, o1],
+        [p1, o2, p0, o1],
+        [p1, o3, (p0, p0), o3],
+        [p2, o0, p2, [(0, 0, 0)] * 2],
+        [p2, o1, p2, [o1] * 2],
+        [p2, o2, p2, [o1] * 2],
+        [p2, o3, p2, o3],
+        [p2, o4, p2 + [(1, 1, 1)], o4],  # uneven paths
+    ]
+    return init_test_data
+
+
+@pytest.mark.parametrize(
+    "init_position, init_orientation_rotvec, expected_position, expected_orientation_rotvec",
+    get_init_pos_orient_test_data(),
+    ids=[f"{ind+1:02d}" for ind, t in enumerate(get_init_pos_orient_test_data())],)
+def test_BaseGeo_init(
+    init_position,
+    init_orientation_rotvec,
+    expected_position,
+    expected_orientation_rotvec,):
+    """test position and orientation initialization"""
+    # print(init_position, init_orientation_rotvec, expected_position, expected_orientation_rotvec)
+    if init_orientation_rotvec is None:
+        init_orientation_rotvec = (0, 0, 0)
+    src = magpy.magnet.Cuboid(
+        (1, 0, 0),
+        (1, 1, 1),
+        position=init_position,
+        orientation=R.from_rotvec(init_orientation_rotvec),)
+    validate_pos_orient(src, expected_position, expected_orientation_rotvec)
+
+
+
+
+############################################################################
+############################################################################
+# BASEGEO POS/ORI SETTER TESTING
+# when pos/ori is set then ori/pos is edge-padded / end-sliced to similar shape.
+
+def get_data_object_setter(inp):
+    """
+    returns data for object setter tests
+
+    init_pos, init_ori, test_pos, test_ori
+    """
+    # test positions
+    p1 = (1,2,3)
+    p3 = [(2,3,4), (3,4,5), (4,5,6)]
+
+    # test orientations
+    o1 = (.1,.2,.3)
+    o3 = [(.1,.2,.3), (.2,.3,.4), (.3,.4,.5)]
+
+    # init states
+    P1 = (1,1,1)
+    O1 = (.1,.1,.1)
+    P2 = [(2,2,2), (3,3,3)]
+    O2 = [(.2,.2,.2), (.3,.3,.3)]
+    P3 = [(2,2,2), (3,3,3), (4,4,4)]
+    O3 = [(.2,.2,.2), (.3,.3,.3), (.4,.4,.4)]
+    P4 = [(2,2,2), (3,3,3), (4,4,4), (5,5,5)]
+    O4 = [(.2,.2,.2), (.3,.3,.3), (.4,.4,.4), (.5,.5,.5)]
+
+    test_data_pos = [
+        # position init, orientation init, set/test position, test orientation
+        (P1, O1, p1, O1),
+        (P1, O1, p3, [O1]*3),            # edge-pad
+        (P2, O2, p1, O2[1]),             # end-slice
+        (P2, O2, p3, O2 + [(.3,.3,.3)]), # edge-pad
+        (P3, O3, p1, O3[2]),             # end-slice
+        (P3, O3, p3, O3),
+        (P4, O4, p1, O4[3]),             # end-slice
+        (P4, O4, p3, O4[1:]),            # end-slice
+    ]
+
+    test_data_ori = [
+        # position init, orientation init, set/test position, test orientation
+        (P1, O1, P1, o1),
+        (P1, O1, [P1]*3, o3),            # edge-pad
+        (P2, O2, P2[1], o1),             # end-slice
+        (P2, O2, P2+[P2[1]], o3),        # edge-pad
+        (P3, O3, P3[-1], o1),            # end-slice
+        (P3, O3, P3, o3),
+        (P4, O4, P4[-1], o1),            # end-slice
+        (P4, O4, P4[1:], o3),            # end-slice
+    ]
+    if inp == 'pos':
+        return test_data_pos
+    return test_data_ori
+
+
+@pytest.mark.parametrize(
+    "init_pos, init_ori, test_pos, test_ori",
+    get_data_object_setter('pos'),
+    ids=[f'{ind+1:02d}' for ind, _ in enumerate(get_data_object_setter('pos'))],)
+def test_BaseGeo_setting_position(init_pos, init_ori, test_pos, test_ori,):
+    """test position and orientation initialization"""
+    src = magpy.magnet.Cuboid((1,0,0), (1,1,1), init_pos, R.from_rotvec(init_ori))
+    src.position = test_pos
+    validate_pos_orient(src, test_pos, test_ori)
+
+
+@pytest.mark.parametrize(
+    "init_pos, init_ori, test_pos, test_ori",
+    get_data_object_setter('ori'),
+    ids=[f'{ind+1:02d}' for ind, _ in enumerate(get_data_object_setter('ori'))],)
+def test_BaseGeo_setting_orientation(init_pos, init_ori, test_pos, test_ori,):
+    """test position and orientation initialization"""
+    src = magpy.magnet.Cuboid((1,0,0), (1,1,1), init_pos, R.from_rotvec(init_ori))
+    src.orientation = R.from_rotvec(test_ori)
+    validate_pos_orient(src, test_pos, test_ori)
+
+
+###############################################################################
+###############################################################################
+# BASEGEO MULTI-ANCHOR ROTATION TESTING
+
+def get_data_BaseGeo_multianchor_rotation():
+    """get test data as dictionaries for multi anchor testing"""
+    data = [
+        dict(
+            description="scalar path - scalar anchor",
+            init_position=(0, 0, 0),
+            init_orientation_rotvec=None,
+            angle=90,
+            axis="z",
+            anchor=0,
+            start="auto",
+            expected_position=(0, 0, 0),
+            expected_orientation_rotvec=(0, 0, np.pi / 2),
+        ),
+        dict(
+            description="vector path 1 - scalar anchor",
+            init_position=(0, 0, 0),
+            init_orientation_rotvec=None,
+            angle=[90],
+            axis="z",
+            anchor=(1, 0, 0),
+            start="auto",
+            expected_position=[(0, 0, 0), (1, -1, 0)],
+            expected_orientation_rotvec=[(0, 0, 0), (0, 0, np.pi / 2)],
+        ),
+        dict(
+            description="vector path 2 - scalar anchor",
+            init_position=(0, 0, 0),
+            init_orientation_rotvec=None,
+            angle=[90, 270],
+            axis="z",
+            anchor=(1, 0, 0),
+            start="auto",
+            expected_position=[(0, 0, 0), (1, -1, 0), (1, 1, 0)],
+            expected_orientation_rotvec=[
+                (0, 0, 0),
+                (0, 0, np.pi / 2),
+                (0, 0, -np.pi / 2),
+            ],
+        ),
+        dict(
+            description="scalar path - vector anchor 1",
+            init_position=(0, 0, 0),
+            init_orientation_rotvec=None,
+            angle=90,
+            axis="z",
+            anchor=[(1, 0, 0)],
+            start="auto",
+            expected_position=[(0, 0, 0), (1, -1, 0)],
+            expected_orientation_rotvec=[(0, 0, 0), (0, 0, np.pi / 2)],
+        ),
+        dict(
+            description="scalar path - vector anchor 2",
+            init_position=(0, 0, 0),
+            init_orientation_rotvec=None,
+            angle=90,
+            axis="z",
+            anchor=[(1, 0, 0), (2, 0, 0)],
+            start="auto",
+            expected_position=[(0, 0, 0), (1, -1, 0), (2, -2, 0)],
+            expected_orientation_rotvec=[
+                (0, 0, 0),
+                (0, 0, np.pi / 2),
+                (0, 0, np.pi / 2),
+            ],
+        ),
+        dict(
+            description="vector path 2 - vector anchor 2",
+            init_position=(0, 0, 0),
+            init_orientation_rotvec=None,
+            angle=[90, 270],
+            axis="z",
+            anchor=[(1, 0, 0), (2, 0, 0)],
+            start="auto",
+            expected_position=[(0, 0, 0), (1, -1, 0), (2, 2, 0)],
+            expected_orientation_rotvec=[
+                (0, 0, 0),
+                (0, 0, np.pi / 2),
+                (0, 0, -np.pi / 2),
+            ],
+        ),
+        dict(
+            description="vector path 2 - vector anchor 2 - path 2 - start=0",
+            init_position=[(0, 0, 0), (2, 1, 0)],
+            init_orientation_rotvec=None,
+            angle=[90, 270],
+            axis="z",
+            anchor=[(1, 0, 0), (2, 0, 0)],
+            start=0,
+            expected_position=[(1, -1, 0), (3, 0, 0)],
+            expected_orientation_rotvec=[(0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+        ),
+        dict(
+            description="init crazy, path 2, anchor 3, start middle",
+            init_position=[(0, 0, 0), (2, 0, 0)],
+            init_orientation_rotvec=(0, 0, 0.1),
+            angle=[90, 270],
+            axis="z",
+            anchor=[(1, 0, 0), (2, 0, 0), (3, 0, 0)],
+            start=1,
+            expected_position=[(0, 0, 0), (1, 1, 0), (2, 0, 0), (3, 1, 0)],
+            expected_orientation_rotvec=[
+                (0, 0, 0.1),
+                (0, 0, np.pi / 2 + 0.1),
+                (0, 0, -np.pi / 2 + 0.1),
+                (0, 0, -np.pi / 2 + 0.1),
+            ],
+        ),
+        dict(
+            description="init crazy, path 2, anchor 3, start before",
+            init_position=[(0, 0, 0), (2, 0, 0)],
+            init_orientation_rotvec=(0, 0, 0.1),
+            angle=[90, 270],
+            axis="z",
+            anchor=[(1, 0, 0), (2, 0, 0), (3, 0, 0)],
+            start=-4,
+            expected_position=[(1, -1, 0), (2, 2, 0), (3, 3, 0), (2, 0, 0)],
+            expected_orientation_rotvec=[
+                (0, 0, 0.1 + np.pi / 2),
+                (0, 0, 0.1 - np.pi / 2),
+                (0, 0, 0.1 - np.pi / 2),
+                (0, 0, 0.1),
+            ],
+        ),
+    ]
+    return data
+
+
+@pytest.mark.parametrize(
+    (
+        "description",
+        "init_position",
+        "init_orientation_rotvec",
+        "angle",
+        "axis",
+        "anchor",
+        "start",
+        "expected_position",
+        "expected_orientation_rotvec",
+    ),
+    [d.values() for d in get_data_BaseGeo_multianchor_rotation()],
+    ids=[d["description"] for d in get_data_BaseGeo_multianchor_rotation()],
+)
+def test_BaseGeo_multianchor_rotation(
+    description,
+    init_position,
+    init_orientation_rotvec,
+    angle,
+    axis,
+    anchor,
+    start,
+    expected_position,
+    expected_orientation_rotvec,
+):
+    """testing BaseGeo multi ancho rotations"""
+    print(description)
+    # print(locals())
+    if init_orientation_rotvec is None:
+        init_orientation_rotvec = (0, 0, 0)
+    src = magpy.magnet.Cuboid(
+        (1, 0, 0),
+        (1, 1, 1),
+        position=init_position,
+        orientation=R.from_rotvec(init_orientation_rotvec),
+    )
+    src.rotate_from_angax(angle, axis, anchor, start)
+    validate_pos_orient(src, expected_position, expected_orientation_rotvec)
\ No newline at end of file
diff --git a/tests/test_obj_Collection.py b/tests/test_obj_Collection.py
index 13bf4998c..37c71d64d 100644
--- a/tests/test_obj_Collection.py
+++ b/tests/test_obj_Collection.py
@@ -82,7 +82,7 @@ def test_Collection_basics():
             for pm in [pm1b, pm2b, pm3b, pm4b, pm5b, pm6b]:
                 pm.move(mv).rotate_from_angax(a, aa, aaa).rotate(rot, aaa)
 
-            col1.move(mv).rotate_from_angax(a, aa, aaa).rotate(rot, aaa)
+            col1.move(mv).rotate_from_angax(a, aa, aaa, start=-1).rotate(rot, aaa, start=-1)
 
         B1 += [magpy.getB([pm1b, pm2b, pm3b, pm4b, pm5b, pm6b], poso, sumup=True)]
         B2 += [col1.getB(poso)]
@@ -119,6 +119,10 @@ def test_Collection_basics():
     ],
 )
 def test_col_getB(test_input, expected):
+    """ testing some Collection stuff with getB"""
+    # pylint: disable=unused-variable
+    # pylint: disable=eval-used
+
     src1 = magpy.magnet.Cuboid(
         magnetization=(1, 0, 1), dimension=(8, 4, 6), position=(0, 0, 0)
     )
@@ -175,12 +179,16 @@ def test_col_getB(test_input, expected):
     ],
 )
 def test_bad_col_getB_inputs(test_input, expected):
+    """more undocumented Collection checking"""
+    # pylint: disable=unused-variable
+    # pylint: disable=eval-used
+
     src1 = magpy.magnet.Cuboid(
-        magnetization=(1, 0, 1), dimension=(8, 4, 6), position=(0, 0, 0)
-    )
+        magnetization=(1, 0, 1), dimension=(8, 4, 6), position=(0, 0, 0))
+
     src2 = magpy.magnet.Cylinder(
-        magnetization=(0, 1, 0), dimension=(8, 5), position=(-15, 0, 0)
-    )
+        magnetization=(0, 1, 0), dimension=(8, 5), position=(-15, 0, 0))
+
     sens1 = magpy.Sensor(position=(0, 0, 6))
     sens2 = magpy.Sensor(position=(0, 0, 6))
     sens3 = magpy.Sensor(position=(0, 0, 6))
@@ -216,6 +224,7 @@ def test_col_getH():
 
 def test_col_reset_path():
     """testing display"""
+    # pylint: disable=no-member
     pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3))
     pm2 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3))
     col = magpy.Collection(pm1, pm2)
@@ -223,6 +232,7 @@ def test_col_reset_path():
     col.reset_path()
     assert col[0].position.ndim == 1, "col reset path fail"
     assert col[1].position.ndim == 1, "col reset path fail"
+    assert col.position.ndim == 1, "col reset path fail"
 
 
 def test_Collection_squeeze():
@@ -284,15 +294,15 @@ def test_adding_sources():
     assert strs == "CubCylSphLooLinDip"
 
 
-def test_set_styles():
+def test_set_children_styles():
     """test if styles get applied"""
     src1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3))
     src2 = magpy.magnet.Cylinder((1, 2, 3), (1, 2))
     col = src1 + src2
-    col.set_styles(magnetization_show=False)
+    col.set_children_styles(magnetization_show=False)
     assert (
         src1.style.magnetization.show is False
         and src1.style.magnetization.show is False
     ), """failed updating styles to children"""
     with pytest.raises(ValueError):
-        col.set_styles(bad_input="somevalue")
+        col.set_children_styles(bad_input="somevalue")
diff --git a/tests/test_obj_Collection_v4motion.py b/tests/test_obj_Collection_v4motion.py
new file mode 100644
index 000000000..516d74368
--- /dev/null
+++ b/tests/test_obj_Collection_v4motion.py
@@ -0,0 +1,636 @@
+import pytest
+import numpy as np
+from scipy.spatial.transform import Rotation as R
+import magpylib as magpy
+
+
+###############################################################################
+###############################################################################
+# NEW COLLECTION POS/ORI TESTS FROM v4
+
+
+def validate_pos_orient(obj, ppath, opath_as_rotvec):
+    """ test position (ppath) and orientation (opath) of BaseGeo object (obj) """
+    sp = obj.position
+    so = obj.orientation
+    ppath = np.array(ppath)
+    opath = R.from_rotvec(opath_as_rotvec)
+    assert ppath.shape == sp.shape, (
+        "position shapes do not match"
+        f"\n object has {sp.shape} instead of {ppath.shape}"
+    )
+    assert opath.as_rotvec().shape == so.as_rotvec().shape, (
+        "orientation as_rotvec shapes do not match"
+        f"\n object has {so.as_rotvec().shape} instead of {opath.as_rotvec().shape}"
+    )
+    assert np.allclose(sp, ppath), (
+        f"position validation failed with ({sp})" f"\n expected {ppath}"
+    )
+    assert np.allclose(so.as_matrix(), opath.as_matrix()), (
+        f"orientation validation failed with ({so.as_rotvec()})"
+        f"\n expected {opath_as_rotvec}"
+    )
+
+
+############################################################################
+############################################################################
+# COLLECTION POS/ORI SETTER TESTING
+# when setting pos/ori of a collection, the children retain their original
+# relative position and orientation in the Collection
+
+
+def get_data_collection_position_setter():
+    """
+    returns data for collection setter tests
+    Args:
+    col_pos_init, col_ori_init, src_pos_init, src_ori_init
+    col_pos_test, col_ori_test, src_pos_test, src_ori_test
+    """
+    data_pos = [
+        [
+            (1, 2, 3),
+            (0.1, 0.2, 0.3),
+            (1, 1, 1),
+            (0, 0, -0.1),
+            (3, 2, 1),
+            (0.1, 0.2, 0.3),
+            (3, 1, -1),
+            (0, 0, -0.1),
+        ],
+        [
+            [(1, 2, 3), (2, 3, 4)],
+            [(0, 0, 0)] * 2,
+            [(1, 1, 1), (2, 2, 2)],
+            [(0.1, 0.1, 0.1), (0.2, 0.2, 0.2)],
+            (4, 5, 6),
+            (0, 0, 0),
+            (4, 4, 4),
+            (0.2, 0.2, 0.2),
+        ],
+        [
+            [(1, 2, 3), (2, 3, 4)],
+            [(0, 0, 0)] * 2,
+            [(1, 1, 1), (2, 2, 2)],
+            [(0.1, 0.1, 0.1), (0.2, 0.2, 0.2)],
+            [(4, 5, 6), (5, 6, 7), (6, 7, 8)],
+            [(0, 0, 0)] * 3,
+            [(4, 4, 4), (5, 5, 5), (6, 6, 6)],
+            [(0.1, 0.1, 0.1), (0.2, 0.2, 0.2), (0.2, 0.2, 0.2)],
+        ],
+        [
+            (1, 2, 3),
+            (0, 0, 0),
+            [(1, 1, 1), (2, 2, 2)],
+            [(0.1, 0.1, 0.1)],
+            [(4, 5, 6), (5, 6, 7), (6, 7, 8)],
+            [(0, 0, 0)] * 3,
+            [(4, 4, 4), (6, 6, 6), (7, 7, 7)],
+            [(0.1, 0.1, 0.1)] * 3,
+        ],
+    ]
+    return data_pos
+
+
+@pytest.mark.parametrize(
+    """col_pos_init, col_ori_init, src_pos_init, src_ori_init,
+    col_pos_test, col_ori_test, src_pos_test, src_ori_test""",
+    get_data_collection_position_setter(),
+    ids=[f"{ind+1:02d}" for ind, _ in enumerate(get_data_collection_position_setter())],
+)
+def test_Collection_setting_position(
+    col_pos_init,
+    col_ori_init,
+    src_pos_init,
+    src_ori_init,
+    col_pos_test,
+    col_ori_test,
+    src_pos_test,
+    src_ori_test,
+):
+    src = magpy.magnet.Cuboid(
+        (1, 0, 0), (1, 1, 1), src_pos_init, R.from_rotvec(src_ori_init)
+    )
+    col = magpy.Collection(
+        src, position=col_pos_init, orientation=R.from_rotvec(col_ori_init)
+    )
+    col.position = col_pos_test
+    validate_pos_orient(col, col_pos_test, col_ori_test)
+    validate_pos_orient(src, src_pos_test, src_ori_test)
+
+
+def get_data_collection_orientation_setter():
+    """
+    returns data for collection setter tests
+    Args:
+    col_pos_init, col_ori_init, src_pos_init, src_ori_init
+    col_pos_test, col_ori_test, src_pos_test, src_ori_test
+    """
+    data_ori = [
+        # col orientation setter simple
+        [
+            (1, 0, 3),
+            (0, 0, np.pi / 4),
+            (2, 0, 1),
+            (0, 0, 0.1),
+            (1, 0, 3),
+            (0, 0, -np.pi / 4),
+            (1, -1, 1),
+            (0, 0, -np.pi / 2 + 0.1),
+        ],
+        # collection orientation setter with path
+        [
+            [(1, 0, 3), (2, 0, 3)],
+            [(0, 0, 0)] * 2,
+            [(2, 0, 1), (1, 0, 1)],
+            [(0, 0, 0)] * 2,
+            [(1, 0, 3), (2, 0, 3)],
+            [(0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+            [(1, 1, 1), (2, 1, 1)],
+            [(0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+        ],
+        # collection orientation setter slice test
+        [
+            [(1, 0, 3), (2, 0, 3), (3, 0, 3)],
+            [(0, 0, 0)] * 3,
+            [(2, 0, 1), (1, 0, 1), (0, 0, 1)],
+            (0, 0, 0),
+            [(2, 0, 3), (3, 0, 3)],
+            [(0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+            [(2, -1, 1), (3, 3, 1)],
+            [(0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+        ],
+        # collection orientation setter pad test
+        [
+            (3, 0, 3),
+            (0, 0, 0),
+            (0, 0, 1),
+            (0, 0, 0),
+            [(3, 0, 3)] * 2,
+            [(0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+            [(3, -3, 1), (3, 3, 1)],
+            [(0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+        ],
+        # crazy collection test with different path formats
+        [
+            [(0, 0, 0), (-1, 0, 0)],
+            [(0, 0, 0)] * 2,
+            (0, 0, 0),
+            (0, 0, 0.1),
+            (-1, 0, 0),
+            (0, 0, np.pi / 2),
+            (-1, 1, 0),
+            (0, 0, np.pi / 2 + 0.1),
+        ],
+        # crazy collection test with different path formats pt2
+        [
+            [(0, 0, 0), (-1, 0, 0)],
+            [(0, 0, 0)] * 2,
+            [(1, 0, 0), (2, 0, 0), (3, 0, 0)],
+            [(0, 0, 0)] * 3,
+            (-1, 0, 0),
+            (0, 0, np.pi / 2),
+            (-1, 4, 0),
+            (0, 0, np.pi / 2),
+        ],
+    ]
+    return data_ori
+
+
+@pytest.mark.parametrize(
+    "col_pos_init, col_ori_init, src_pos_init, src_ori_init, col_pos_test, col_ori_test, src_pos_test, src_ori_test",
+    get_data_collection_orientation_setter(),
+    ids=[
+        f"{ind+1:02d}" for ind, _ in enumerate(get_data_collection_orientation_setter())
+    ],
+)
+def test_Collection_setting_orientation(
+    col_pos_init,
+    col_ori_init,
+    src_pos_init,
+    src_ori_init,
+    col_pos_test,
+    col_ori_test,
+    src_pos_test,
+    src_ori_test,
+):
+    src = magpy.magnet.Cuboid(
+        (1, 0, 0), (1, 1, 1), src_pos_init, R.from_rotvec(src_ori_init)
+    )
+    col = magpy.Collection(
+        src, position=col_pos_init, orientation=R.from_rotvec(col_ori_init)
+    )
+    col.orientation = R.from_rotvec(col_ori_test)
+    validate_pos_orient(col, col_pos_test, col_ori_test)
+    validate_pos_orient(src, src_pos_test, src_ori_test)
+
+
+def test_Collection_setter():
+    """
+    general col position and orientation setter testing
+    """
+    # graphical test: is the Collection moving/rotating as a whole ?
+    # col0 = magpy.Collection()
+    # for poz,roz in zip(
+    #     [(0,0,0), (0,0,5), (5,0,0), (5,0,5), (10,0,0), (10,0,5)],
+    #     [(0,0,0), (1,0,0), (0,1,0), (0,0,1), (1,2,3), (-2,-1,3)]
+    #     ):
+    #     col = magpy.Collection()
+    #     for i,color in enumerate(['r', 'orange', 'gold', 'green', 'cyan']):
+    #         src = magpy.magnet.Cuboid((1,0,0), (.5,.5,.5), (1,0,0), style_color=color)
+    #         src.rotate_from_angax(72*i, 'z', (0,0,0))
+    #         col = col + src
+    #     base = magpy.Sensor()
+    #     col.position = poz
+    #     col.orientation = R.from_rotvec(roz)
+    #     base.position = poz
+    #     base.orientation = R.from_rotvec(roz)
+    #     col0 = col0 + col + base
+    # magpy.display(*col0)
+    POS = []
+    ORI = []
+    col0 = magpy.Collection()
+    for poz, roz in zip(
+        [(0, 0, 0), (0, 0, 5), (5, 0, 0), (5, 0, 5), (10, 0, 0), (10, 0, 5)],
+        [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 2, 3), (-2, -1, 3)],
+    ):
+        col = magpy.Collection()
+        for i in range(5):
+            src = magpy.magnet.Cuboid((1, 0, 0), (0.5, 0.5, 0.5), (1, 0, 0))
+            src.rotate_from_angax(72 * i, "z", (0, 0, 0))
+            col = col + src
+        col.position = poz
+        col.orientation = R.from_rotvec(roz)
+
+        col0 = col0 + col
+
+        POS += [[src.position for src in col]]
+        ORI += [[src.orientation.as_rotvec() for src in col]]
+
+    test_POS, test_ORI = np.load("tests/testdata/testdata_Collection_setter.npy")
+
+    assert np.allclose(POS, test_POS)
+    assert np.allclose(ORI, test_ORI)
+
+
+############################################################################
+############################################################################
+# COLLECTION MOTION TESTS
+# An operation move() or rotate() applied to a Collection is
+# individually applied to BaseGeo and to each child:
+
+
+def test_compound_motion_00():
+    """init Collection should not change source pos and ori"""
+    src = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 2, 3), (2, 3, 4)])
+    validate_pos_orient(src, [(1, 2, 3), (2, 3, 4)], [(0, 0, 0)] * 2)
+    col = magpy.Collection(src, position=[(1, 1, 1)])
+    validate_pos_orient(src, [(1, 2, 3), (2, 3, 4)], [(0, 0, 0)] * 2)
+    print(col)
+
+
+def test_compound_motion_01():
+    """very sensible Compound behavior with rotation anchor"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 1, 1))
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (-1, -1, -1))
+    col = magpy.Collection(s1, s2)
+    col.move((0, 0, 1))
+    validate_pos_orient(s1, (1, 1, 2), (0, 0, 0))
+    validate_pos_orient(s2, (-1, -1, 0), (0, 0, 0))
+    validate_pos_orient(col, (0, 0, 1), (0, 0, 0))
+    col.move([(0, 0, 1)])
+    validate_pos_orient(s1, [(1, 1, 2), (1, 1, 3)], [(0, 0, 0)] * 2)
+    validate_pos_orient(s2, [(-1, -1, 0), (-1, -1, 1)], [(0, 0, 0)] * 2)
+    validate_pos_orient(col, [(0, 0, 1), (0, 0, 2)], [(0, 0, 0)] * 2)
+    col.rotate_from_rotvec((0, 0, np.pi / 2), anchor=0)
+    validate_pos_orient(s1, [(-1, 1, 2), (-1, 1, 3)], [(0, 0, np.pi / 2)] * 2)
+    validate_pos_orient(s2, [(1, -1, 0), (1, -1, 1)], [(0, 0, np.pi / 2)] * 2)
+    validate_pos_orient(col, [(0, 0, 1), (0, 0, 2)], [(0, 0, np.pi / 2)] * 2)
+
+
+def test_compound_motion_02():
+    """very sensible Compound behavior with vector anchor"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1))
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (-1, 0, -1))
+    col = magpy.Collection(s1, s2, position=(3, 0, 3))
+    col.rotate_from_rotvec((0, 0, np.pi / 2), anchor=[(1, 0, 0), (2, 0, 0)])
+    validate_pos_orient(
+        s1,
+        [(1, 0, 1), (1, 0, 1), (2, -1, 1)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, np.pi / 2)],
+    )
+    validate_pos_orient(
+        s2,
+        [(-1, 0, -1), (1, -2, -1), (2, -3, -1)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, np.pi / 2)],
+    )
+    validate_pos_orient(
+        col,
+        [(3, 0, 3), (1, 2, 3), (2, 1, 3)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, np.pi / 2)],
+    )
+
+
+def test_compound_motion_03():
+    """very sensible Compound behavior with vector path and anchor and start=0"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(3, 0, 0), (1, 0, 0)])
+    s2 = magpy.magnet.Cuboid(
+        (1, 0, 0),
+        (1, 1, 1),
+        [(2, 0, 2), (2, 0, 2)],
+        R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)]),
+    )
+    col = magpy.Collection(s1, s2, position=[(3, 0, 2), (3, 0, 3)])
+    col.rotate_from_rotvec(
+        [(0, 0, np.pi / 2), (0, 0, 3 * np.pi / 2)],
+        anchor=[(1, 0, 0), (2, 0, 0)],
+        start=0,
+    )
+    validate_pos_orient(
+        s1, [(1, 2, 0), (2, 1, 0)], [(0, 0, np.pi / 2), (0, 0, -np.pi / 2)]
+    )
+    validate_pos_orient(
+        s2, [(1, 1, 2), (2, 0, 2)], [(0, 0, np.pi / 2 - 0.1), (0, 0, -np.pi / 2 - 0.2)]
+    )
+    validate_pos_orient(
+        col, [(1, 2, 2), (2, -1, 3)], [(0, 0, np.pi / 2), (0, 0, -np.pi / 2)]
+    )
+
+
+def test_compound_motion_04():
+    """nonsensical but correct Collection behavior when col and children
+    all have different path formats"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), position=(1, 1, 1))
+    s2 = magpy.magnet.Cuboid(
+        (1, 0, 0), (1, 1, 1), orientation=R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)])
+    )
+    col = magpy.Collection(s1, s2, position=[(1, 2, 3), (1, 3, 4)])
+    col.rotate_from_angax(90, "z", anchor=(1, 0, 0))
+    validate_pos_orient(s1, (0, 0, 1), (0, 0, np.pi / 2))
+    validate_pos_orient(
+        s2, [(1, -1, 0)] * 2, [(0, 0, np.pi / 2 - 0.1), (0, 0, np.pi / 2 - 0.2)]
+    )
+    validate_pos_orient(col, [(-1, 0, 3), (-2, 0, 4)], [(0, 0, np.pi / 2)] * 2)
+
+
+def test_compound_motion_05():
+    """nonsensical but correct Collection behavior with vector anchor"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), position=(1, 0, 1))
+    s2 = magpy.magnet.Cuboid(
+        (1, 0, 0), (1, 1, 1), orientation=R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)])
+    )
+    col = magpy.Collection(s1, s2, position=[(3, 0, 3), (4, 0, 4)])
+    col.rotate_from_angax(90, "z", anchor=[(1, 0, 0), (2, 0, 0)])
+    validate_pos_orient(
+        s1,
+        [(1, 0, 1), (1, 0, 1), (2, -1, 1)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, np.pi / 2)],
+    )
+    validate_pos_orient(
+        s2,
+        [(0, 0, 0), (0, 0, 0), (1, -1, 0), (2, -2, 0)],
+        [(0, 0, -0.1), (0, 0, -0.2), (0, 0, np.pi / 2 - 0.2), (0, 0, np.pi / 2 - 0.2)],
+    )
+    validate_pos_orient(
+        col,
+        [(3, 0, 3), (4, 0, 4), (1, 3, 4), (2, 2, 4)],
+        [(0, 0, 0), (0, 0, 0), (0, 0, np.pi / 2), (0, 0, np.pi / 2)],
+    )
+
+
+def test_compound_motion_06():
+    """Compound rotation (anchor=None), scalar input, scalar pos"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1))
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (0, -1, -1))
+    col = magpy.Collection(s1, s2)
+    col.rotate_from_angax(90, "z")
+    validate_pos_orient(s1, (0, 1, 1), (0, 0, np.pi / 2))
+    validate_pos_orient(s2, (1, 0, -1), (0, 0, np.pi / 2))
+    validate_pos_orient(col, (0, 0, 0), (0, 0, np.pi / 2))
+
+
+def test_compound_motion_07():
+    """Compound rotation (anchor=None), scalar input, vector pos, start=auto"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)])
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)])
+    col = magpy.Collection(s1, s2, position=((0, 0, 0), (1, 0, 0)))
+    col.rotate_from_angax(90, "z")
+    validate_pos_orient(
+        s1, [(0, 1, 0), (1, 1, 0)], [(0, 0, np.pi / 2), (0, 0, np.pi / 2)]
+    )
+    validate_pos_orient(
+        s2, [(0, -1, 0), (1, -3, 0)], [(0, 0, np.pi / 2), (0, 0, np.pi / 2)]
+    )
+    validate_pos_orient(
+        col, [(0, 0, 0), (1, 0, 0)], [(0, 0, np.pi / 2), (0, 0, np.pi / 2)]
+    )
+
+
+def test_compound_motion_08():
+    """Compound rotation (anchor=None), scalar input, vector pos, start=1"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)])
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)])
+    col = magpy.Collection(s1, s2, position=((0, 0, 0), (1, 0, 0)))
+    col.rotate_from_angax(90, "z", start=1)
+    validate_pos_orient(s1, [(1, 0, 0), (1, 1, 0)], [(0, 0, 0), (0, 0, np.pi / 2)])
+    validate_pos_orient(s2, [(-1, 0, 0), (1, -3, 0)], [(0, 0, 0), (0, 0, np.pi / 2)])
+    validate_pos_orient(col, [(0, 0, 0), (1, 0, 0)], [(0, 0, 0), (0, 0, np.pi / 2)])
+
+
+def test_compound_motion_09():
+    """Compound rotation (anchor=None), scalar input, vector pos, start=-1"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)])
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)])
+    col = magpy.Collection(s1, s2, position=((0, 0, 0), (1, 0, 0)))
+    col.rotate_from_angax(90, "z", start=-1)
+    validate_pos_orient(s1, [(1, 0, 0), (1, 1, 0)], [(0, 0, 0), (0, 0, np.pi / 2)])
+    validate_pos_orient(s2, [(-1, 0, 0), (1, -3, 0)], [(0, 0, 0), (0, 0, np.pi / 2)])
+    validate_pos_orient(col, [(0, 0, 0), (1, 0, 0)], [(0, 0, 0), (0, 0, np.pi / 2)])
+
+
+def test_compound_motion_10():
+    """Compound rotation (anchor=None), scalar input, vector pos, start->pad before"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)])
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)])
+    col = magpy.Collection(s1, s2, position=((2, 0, 0), (1, 0, 0)))
+    col.rotate_from_angax(90, "z", start=-4)
+    validate_pos_orient(
+        s1, [(2, -1, 0), (2, -1, 0), (2, -1, 0), (1, 1, 0)], [(0, 0, np.pi / 2)] * 4,
+    )
+    validate_pos_orient(
+        s2, [(2, -3, 0), (2, -3, 0), (2, -3, 0), (1, -3, 0)], [(0, 0, np.pi / 2)] * 4,
+    )
+    validate_pos_orient(
+        col, [(2, 0, 0), (2, 0, 0), (2, 0, 0), (1, 0, 0)], [(0, 0, np.pi / 2)] * 4,
+    )
+
+
+def test_compound_motion_11():
+    """Compound rotation (anchor=None), scalar input, vector pos, start->pad behind"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)])
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)])
+    col = magpy.Collection(s1, s2, position=((2, 0, 0), (1, 0, 0)))
+    col.rotate_from_angax(90, "z", start=3)
+    validate_pos_orient(
+        s1,
+        [(1, 0, 0), (2, 0, 0), (2, 0, 0), (1, 1, 0)],
+        [(0, 0, 0)] * 3 + [(0, 0, np.pi / 2)],
+    )
+    validate_pos_orient(
+        s2,
+        [(-1, 0, 0), (-2, 0, 0), (-2, 0, 0), (1, -3, 0)],
+        [(0, 0, 0)] * 3 + [(0, 0, np.pi / 2)],
+    )
+    validate_pos_orient(
+        col,
+        [(2, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0)],
+        [(0, 0, 0)] * 3 + [(0, 0, np.pi / 2)],
+    )
+
+
+def test_compound_motion_12():
+    """Compound rotation (anchor=None), vector input, simple pos, start=auto"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1))
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (0, -1, -1))
+    col = magpy.Collection(s1, s2)
+    col.rotate_from_angax([90, -90], "z")
+    validate_pos_orient(
+        s1,
+        [(1, 0, 1), (0, 1, 1), (0, -1, 1)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+    )
+    validate_pos_orient(
+        s2,
+        [(0, -1, -1), (1, 0, -1), (-1, 0, -1)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+    )
+    validate_pos_orient(
+        col,
+        [(0, 0, 0), (0, 0, 0), (0, 0, 0)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+    )
+
+
+def test_compound_motion_13():
+    """Compound rotation (anchor=None), vector input, vector pos, start=1"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1))
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (0, -1, -1))
+    col = magpy.Collection(s1, s2)
+    col.rotate_from_angax([90, -90], "z")
+    col.rotate_from_angax([-90, 180], "z", start=1)
+    validate_pos_orient(
+        s1, [(1, 0, 1), (1, 0, 1), (0, 1, 1)], [(0, 0, 0), (0, 0, 0), (0, 0, np.pi / 2)]
+    )
+    validate_pos_orient(
+        s2,
+        [(0, -1, -1), (0, -1, -1), (1, 0, -1)],
+        [(0, 0, 0), (0, 0, 0), (0, 0, np.pi / 2)],
+    )
+    validate_pos_orient(
+        col,
+        [(0, 0, 0), (0, 0, 0), (0, 0, 0)],
+        [(0, 0, 0), (0, 0, 0), (0, 0, np.pi / 2)],
+    )
+
+
+def test_compound_motion_14():
+    """Compound rotation (anchor=None), vector input, vector pos, start=1, pad_behind"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1))
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (0, -1, -1))
+    col = magpy.Collection(s1, s2)
+    col.rotate_from_angax([90, -90], "z")
+    col.rotate_from_angax([-90, 180], "z", start=1)
+    col.rotate_from_angax([90, 180, -90], "z", start=1)
+    validate_pos_orient(
+        s1,
+        [(1, 0, 1), (0, 1, 1), (0, -1, 1), (1, 0, 1)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, 0)],
+    )
+    validate_pos_orient(
+        s2,
+        [(0, -1, -1), (1, 0, -1), (-1, 0, -1), (0, -1, -1)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, 0)],
+    )
+    validate_pos_orient(
+        col,
+        [(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, 0)],
+    )
+
+
+def test_compound_motion_15():
+    """Compound rotation (anchor=None), vector input, simple pos, start=-3, pad_before"""
+    s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1))
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (-1, 0, -1))
+    col = magpy.Collection(s1, s2, position=(2, 0, 0))
+    col.rotate_from_angax([90, -90], "z", start=-3)
+    validate_pos_orient(
+        s1,
+        [(2, -1, 1), (2, 1, 1), (1, 0, 1)],
+        [(0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, 0)],
+    )
+    validate_pos_orient(
+        s2,
+        [(2, -3, -1), (2, 3, -1), (-1, 0, -1)],
+        [(0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, 0)],
+    )
+    validate_pos_orient(
+        col,
+        [(2, 0, 0), (2, 0, 0), (2, 0, 0)],
+        [(0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, 0)],
+    )
+
+
+def test_compound_motion_16():
+    """Compound rotation (anchor=None), vector input, vector pos, start=-3,
+    pad_before AND pad_behind"""
+    s1 = magpy.magnet.Cuboid(
+        (1, 0, 0), (1, 1, 1), orientation=R.from_rotvec([(0, 0, 0.1), (0, 0, 0.2)])
+    )
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), position=[(-1, 0, 0), (-2, 0, 0)])
+    col = magpy.Collection(s1, s2, position=[(1, 0, 0), (0, 0, 0)])
+    col.rotate_from_angax([90, -90, 90, -90], "z", start=-3)
+    validate_pos_orient(
+        s1,
+        [(1, -1, 0), (1, 1, 0), (0, 0, 0), (0, 0, 0)],
+        [
+            (0, 0, 0.1 + np.pi / 2),
+            (0, 0, 0.1 - np.pi / 2),
+            (0, 0, 0.2 + np.pi / 2),
+            (0, 0, 0.2 - np.pi / 2),
+        ],
+    )
+    validate_pos_orient(
+        s2,
+        [(1, -2, 0), (1, 2, 0), (0, -2, 0), (0, 2, 0)],
+        [(0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+    )
+    validate_pos_orient(
+        col,
+        [(1, 0, 0), (1, 0, 0), (0, 0, 0), (0, 0, 0)],
+        [(0, 0, np.pi / 2), (0, 0, -np.pi / 2), (0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+    )
+
+
+def test_compound_motion_17():
+    """CRAZY Compound rotation (anchor=None) with messy path formats"""
+    s1 = magpy.magnet.Cuboid(
+        (1, 0, 0), (1, 1, 1), orientation=R.from_rotvec([(0, 0, 0.1), (0, 0, 0.2)])
+    )
+    s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), position=(-1, 0, 0))
+    col = magpy.Collection(s1, s2, position=[(1, 0, 0), (0, 0, 0), (3, 0, 3)])
+    col.rotate_from_angax([90, -90], "z", start="auto")
+    validate_pos_orient(
+        s1,
+        [(0, 0, 0), (0, 0, 0), (3, -3, 0), (3, 3, 0)],
+        [(0, 0, 0.1), (0, 0, 0.2), (0, 0, 0.2 + np.pi / 2), (0, 0, 0.2 - np.pi / 2)],
+    )
+    validate_pos_orient(
+        s2,
+        [(-1, 0, 0), (3, -4, 0), (3, 4, 0)],
+        [(0, 0, 0), (0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+    )
+    validate_pos_orient(
+        col,
+        [(1, 0, 0), (0, 0, 0), (3, 0, 3), (3, 0, 3), (3, 0, 3)],
+        [(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, np.pi / 2), (0, 0, -np.pi / 2)],
+    )
diff --git a/tests/test_obj_Cuboid.py b/tests/test_obj_Cuboid.py
index bf691c210..1638e51df 100644
--- a/tests/test_obj_Cuboid.py
+++ b/tests/test_obj_Cuboid.py
@@ -47,7 +47,7 @@ def test_Cuboid_basics():
 
         # 18 subsequent operations
         for a,aa,aaa,mv in zip(ang,ax,anch,mov):
-            pm.move(mv).rotate_from_angax(a,aa,aaa)
+            pm.move(mv).rotate_from_angax(a, aa, aaa, start=-1)
 
         btest += [pm.getB(poso)]
     btest = np.array(btest)
diff --git a/tests/test_obj_Sphere.py b/tests/test_obj_Sphere.py
index 974c17ab1..e791496e8 100644
--- a/tests/test_obj_Sphere.py
+++ b/tests/test_obj_Sphere.py
@@ -46,7 +46,7 @@ def test_Sphere_basics():
 
         # 18 subsequent operations
         for a,aa,aaa,mv in zip(ang,ax,anch,mov):
-            pm.move(mv).rotate_from_angax(a,aa,aaa)
+            pm.move(mv).rotate_from_angax(a,aa,aaa,start=-1)
 
         btest += [pm.getB(poso)]
     btest = np.array(btest)
diff --git a/tests/test_path.py b/tests/test_path.py
index 6d59051a3..31f36b5c1 100644
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -10,7 +10,7 @@ def test_path_old_new_move():
 
     # path style code translation
     pm1 = Cylinder((0,0,1000), (3,3), position=(-5,0,3))
-    pm1.move([(x,0,0) for x in np.linspace(0,10,100)])
+    pm1.move([(x,0,0) for x in np.linspace(0,10,100)], start=-1)
     B1 = pm1.getB(s_pos)
 
     # old style code translation
@@ -35,7 +35,7 @@ def test_path_old_new_rotate():
     # path style code rotation
     pm1 = Cuboid((0,0,1000), (1,2,3), position=(0,0,3))
     pm1.rotate_from_angax(-30, ax, anch)
-    pm1.rotate_from_angax(np.linspace(0,60,n), 'x', anch)
+    pm1.rotate_from_angax(np.linspace(0,60,n), 'x', anch, start=-1)
     B1 = pm1.getB(s_pos)
 
     # old style code rotation
diff --git a/tests/test_vs_mag2.py b/tests/test_vs_mag2.py
index 95c463826..a21c9a28a 100644
--- a/tests/test_vs_mag2.py
+++ b/tests/test_vs_mag2.py
@@ -58,7 +58,8 @@ def test_vs_mag2_spiral():
 
     angs = np.linspace(0,297,100)
     pm.rotate_from_angax(angs, 'z', anchor=0, start=0)
-    pm.move([(0,0,.1)]*99, start=1, increment=True)
+    possis = np.linspace((0,0,.1), (0,0,9.9), 99)
+    pm.move(possis, start=1)
     B = pm.getB((0,0,0))
     assert np.allclose(B, data), 'vs mag2 - rot'
 
diff --git a/tests/testdata/testdata_Collection_setter.npy b/tests/testdata/testdata_Collection_setter.npy
new file mode 100644
index 000000000..577593cc8
Binary files /dev/null and b/tests/testdata/testdata_Collection_setter.npy differ
diff --git a/tests/testdata/testdata_compound_setter_cases.npy b/tests/testdata/testdata_compound_setter_cases.npy
new file mode 100644
index 000000000..f64200a92
Binary files /dev/null and b/tests/testdata/testdata_compound_setter_cases.npy differ