--- jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 jupytext_version: 1.13.7 kernelspec: display_name: Python 3 (ipykernel) language: python name: python3 --- (intro)= # Introduction This section gives an introduction to the Magpylib API. More detailed information and practical examples how to use Magpylib can be found in the example galleries. The package documentation is found in the {ref}`modindex`. ## Contents - {ref}`intro-idea` - {ref}`intro-when-to-use` - {ref}`intro-magpylib-objects` - {ref}`intro-position-and-orientation` - {ref}`intro-graphic-output` - {ref}`intro-field-computation` - {ref}`intro-direct-interface` - {ref}`intro-collections` - {ref}`intro-customization` (intro-idea)= ## The idea behind Magpylib Magpylib provides fast and accurate magnetostatic field computation based on **analytical solutions** to permanent magnet and current problems. The field computation is coupled to a **position and orientation interface** that makes it easy to work with relative object positioning. The idea behind the main object oriented interface is: 1. Sensors, magnets, currents, etc. are created as Python objects with defined position and orientation in a global coordinate system. 2. After initialization, the Magpylib objects can easily be manipulated, grouped, moved around and displayed graphically using Matplotlib or Plotly 3D plotting. 3. When all objects are set up, the magnetic field generated by the source objects is computed at the observer objects. The following example code outlines this functionality: ```{code-cell} ipython3 import numpy as np import matplotlib.pyplot as plt import magpylib as magpy # 1. define sources and observers as objects cube = magpy.magnet.Cuboid(magnetization=(0,0,100), dimension=(1,1,1)) loop = magpy.current.Loop(current=5, diameter=3, position=(0,0,-3)) sensor = magpy.Sensor(position=(0,0,2), style_size=1.8) # 2. move objects and display graphically sensor.rotate_from_rotvec((0,0,225), degrees=True) cube.position = np.linspace((-3,0,0), (3,0,0), 50) loop.move(np.linspace((0,0,0), (0,0,6), 50), start=0) magpy.show(loop, cube, sensor, backend='plotly', animation=2, style_path_show=False) # 3. compute field at sensor (and plot with Matplotlib) B = sensor.getB(cube, loop, sumup=True) plt.plot(B, label=['Bx', 'By', 'Bz']) plt.legend() plt.grid(color='.8') plt.show() ``` For users who would like to avoid the object oriented interface, the field implementations can also be accessed directly, see {ref}`intro-direct-interface`. Details on how the analytical solutions are mathematically obtained can be found in the {ref}`physComp` section. (intro-when-to-use)= ## When can you use Magpylib ? The analytical solutions are exact when there is no material response and natural boundary conditions can be assumed. In general, Magpylib is at its best when dealing with air-coils (no eddy currents) and high grade permanent magnets (Ferrite, NdFeB, SmCo or similar materials). When **magnet** permeabilities are below $\mu_r < 1.1$ the error typically undercuts 1-5 % (long magnet shapes are better, large distance from magnet is better). Demagnetization factors are not automatically included at this point. The line **current** solutions give the exact same field as outside of a wire that carries a homogenous current. For more details check out the {ref}`physComp` section. Magpylib only provides solutions for simple geometric forms (cuboids, cylinders, lines, ...). How **complex shapes** can be constructed from these simple base shapes is described in {ref}`examples-complex-forms`. (intro-magpylib-objects)= ## The Magpylib objects The most convenient way of working with Magpylib is through the **object oriented interface**. Magpylib objects represent magnetic field sources, sensors and collections with various defining attributes and methods. By default all objects are initialized with `position=(0,0,0)` and `orientation=None`. The following classes are implemented: **Magnets** All magnet objects have the `magnetization` attribute which must be of the format $(m_x, m_y, m_z)$ and denotes the homogeneous magnetization vector in the local object coordinates in units of \[mT\]. It is often referred to as the remanence ($B_r=\mu_0 M$) in material data sheets. All magnets can be used as Magpylib `sources` input. - **`Cuboid`**`(magnetization, dimension, position, orientation)` represents a magnet with cuboid shape. `dimension` has the format $(a,b,c)$ and denotes the sides of the cuboid in units of \[mm\]. By default the center of the cuboid lies in the origin of the global coordinates, and the sides are parallel to the coordinate axes. - **`Cylinder`**`(magnetization, dimension, position, orientation)` represents a magnet with cylindrical shape. `dimension` has the format $(d,h)$ and denotes diameter and height of the cylinder in units of \[mm\]. By default the center of the cylinder lies in the origin of the global coordinates, and the cylinder axis coincides with the z-axis. - **`CylinderSegment`**`(magnetization, dimension, position, orientation)` represents a magnet with the shape of a cylindrical ring section. `dimension` has the format $(r_1,r_2,h,\varphi_1,\varphi_2)$ and denotes inner radius, outer radius and height in units of \[mm\] and the two section angles $\varphi_1<\varphi_2$ in \[deg\]. By default the center of the full cylinder lies in the origin of the global coordinates, and the cylinder axis coincides with the z-axis. - **`Sphere`**`(magnetization, diameter, position, orientation)` represents a magnet of spherical shape. `diameter` is the sphere diameter $d$ in units of \[mm\]. By default the center of the sphere lies in the origin of the global coordinates. **Currents** All current objects have the `current` attribute which must be a scalar $i_0$ and denotes the electrical current in units of \[A\]. All currents can be used as Magpylib `sources` input. - **`Loop`**`(current, diameter, position, orientation)` represents a circular current loop where `diameter` is the loop diameter $d$ in units of \[mm\]. By default the loop lies in the xy-plane with it's center in the origin of the global coordinates. - **`Line`**`(current, vertices, position, orientation)` represents electrical current segments that flow in a straight line from vertex to vertex. By default the verticx positions coincide in the local object coordinates and the global coordinates. **Other** - **`Dipole`**`(moment, position, orientation)` represents a magnetic dipole moment with moment $(m_x,m_y,m_z)$ given in \[mT mm³]. For homogeneous magnets the relation moment=magnetization$\times$volume holds. Can be used as Magpylib `sources` input. - **`CustomSource`**`(field_func, position, orientation)` is used to create user defined custom sources with their own field functions. Can be used as Magpylib `sources` input. - **`Sensor`**`(position, pixel, orientation)` represents a 3D magnetic field sensor. The field is evaluated at the given pixel positions. By default (`pixel=(0,0,0)`) the pixel position coincide in the local object coordinates and the global coordinates. Can be used as Magpylib `observers` input. - **`Collection`**`(*children, position, orientation)` is a group of source and sensor objects (children) that is used for common manipulation. Depending on the children, a collection can be used as Magpylib `sources` and/or `observers` input. ```{code-cell} ipython3 import magpylib as magpy # magnets magnet1 = magpy.magnet.Cuboid() magnet2 = magpy.magnet.Cylinder() magnet3 = magpy.magnet.CylinderSegment() magnet4 = magpy.magnet.Sphere() # currents current1 = magpy.current.Loop() current2 = magpy.current.Line() # other dipole = magpy.misc.Dipole() custom = magpy.misc.CustomSource() sensor = magpy.Sensor() coll = magpy.Collection() # print object representation for obj in [magnet1, magnet2, magnet3, magnet4, current1, current2, dipole, custom, sensor, coll]: print(obj) ``` (intro-position-and-orientation)= ## Position and orientation All Magpylib objects have the `position` and `orientation` attributes that refer to position and orientation in the global Cartesian coordinate system. The `position` attribute is a numpy ndarray, shape (3,) or (m,3) and denotes the coordinates $(x,y,z)$ in units of [mm]. By default every object is created at `position=(0,0,0)`. The `orientation` attribute is a scipy [Rotation object](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html) and denotes the object rotation relative to its initial state. By default the orientation of a Magpylib object is the unit rotation, `orientation=None`. ```python import magpylib as magpy sensor = magpy.Sensor() print(sensor.position) # out: [0. 0. 0.] print(sensor.orientation.as_euler('xyz', degrees=True)) # out: [0. 0. 0.] ``` Set absolute object position/orientation at initialization or through the properties directly. Add relative position/orientation with the `move` and `rotate` methods, ```python import magpylib as magpy from scipy.spatial.transform import Rotation as R # set at initialization sensor = magpy.Sensor(position=(1,1,1)) print(sensor.position) # out: [1. 1. 1.] print(sensor.orientation.as_euler('xyz', degrees=True)) # out: [0. 0. 0.] # set properties directly sensor.orientation = R.from_rotvec((0,0,45), degrees=True) print(sensor.position) # out: [1. 1. 1.] print(sensor.orientation.as_euler('xyz', degrees=True)) # out: [ 0. 0. 45.] # move and rotate sensor.move((1,2,3)) sensor.rotate_from_angax(45, 'z') print(sensor.position) # out: [2. 3. 4.] print(sensor.orientation.as_euler('xyz', degrees=True)) # out: [ 0. 0. 90.] ``` The attributes `position` and `orientation` can be either of **"scalar"** nature, i.e. a single position or a single rotation like in the examples above, or **"vectors"** when they are arrays of such scalars. The two attributes together define an object **"path"**. Paths should always be used when modelling object motion as the magnetic field is computed on the whole path with increased performance. With vector inputs, the `move` and `rotate` methods provide *append* and *merge* functionality. The following example shows how a path `path1` is assigned to a magnet object, how `path2` is appended with `move` and how `path3` is merged on top starting at path index 25. ```{code-cell} ipython3 import numpy as np from magpylib.magnet import Cylinder magnet = Cylinder(magnetization=(100,0,0), dimension=(2,2)) # assign path path1 = np.linspace((0,0,0), (0,0,5), 20) magnet.position = path1 # append path path2 = np.linspace((0,0,0), (0,10,0), 40) magnet.move(path2[1:]) # merge path path3 = np.linspace(0, 360, 20) magnet.rotate_from_angax(angle=path3, axis='z', anchor=0, start=25) magnet.show(backend='plotly') ``` Notice that when one of the `position` and `orientation` attributes are modified in length, the other is automatically adjusted to the same length. A detailed outline of the functionality of `position`, `orientation`, `move`, `rotate` and paths is given in {ref}`examples-paths`. (intro-graphic-output)= ## Graphic output Once all Magpylib objects and their paths have been created, **`show`** provides a convenient way to graphically display the geometric arrangement using the Matplotlib (default) and Plotly packages. When `show` is called, it generates a new figure which is then automatically displayed. The desired graphic backend is selected with the `backend` keyword argument. To bring the output to a given, user-defined figure, the `canvas` kwarg is used. This is demonstrated in {ref}`examples-backends-canvas`. The following example shows the graphical representation of various Magpylib objects and their paths using the default Matplotlib graphic backend. ```{code-cell} ipython3 import numpy as np import magpylib as magpy from magpylib.magnet import Cuboid, Cylinder, CylinderSegment, Sphere from magpylib.current import Loop, Line from magpylib.misc import Dipole objects = [ Cuboid( magnetization=(0,100,0), dimension=(1,1,1), position=(-7,0,0), ), Cylinder( magnetization=(0,0,100), dimension=(1,1), position=(-5,0,0), ), CylinderSegment( magnetization=(0,0,100), dimension=(.3,1,1,0,140), position=(-3,0,0), ), Sphere( magnetization=(0,0,100), diameter=1, position=(-1,0,0), ), Loop( current=1, diameter=1, position=(1,0,0), ), Line( current=1, vertices=[(1,0,0), (0,1,0), (-1,0,0), (0,-1,0), (1,0,0)], position=(3,0,0), ), Dipole( moment=(0,0,100), position=(5,0,0), ), magpy.Sensor( pixel=[(0,0,z) for z in (-.5,0,.5)], position=(7,0,0), ), ] objects[5].move(np.linspace((0,0,0), (0,0,7), 20)) objects[0].rotate_from_angax(np.linspace(0, -90, 20), 'y', anchor=0) magpy.show(objects) ``` Notice that, objects and their paths are automatically assigned different colors, the magnetization vector, current directions and dipole objects are indicated by arrows and sensors are shown as tri-colored coordinate cross with pixel as markers. How objects are represented graphically (color, line thickness, ect.) is defined by the **style**. The default style, which can be seen above, is accessed and manipulated through `magpy.defaults.display.style`. In addition, each object can have an individual style, which takes precedence over the default setting. A local style override is also possible by passing style arguments directly to `show`. Some practical ways to set styles are shown in the next example: ```{code-cell} ipython3 import magpylib as magpy from magpylib.magnet import Cuboid cube1 = Cuboid(magnetization=(0,0,1), dimension=(2,4,4)) cube2 = cube1.copy(position=(3,0,0)) cube3 = cube1.copy(position=(6,0,0)) # change the default magpy.defaults.display.style.base.color = 'crimson' # set individual style through properties cube2.style.color = 'orangered' # set individual style using update with style dictionary cube3.style.update({'color': 'gold'}) # set individual style at initialization with underscore magic cube4 = cube1.copy(position=(9,0,0), style_color='linen') # show with local style override magpy.show(cube1, cube2, cube3, cube4, style_magnetization_show=False) ``` The hierarchy that decides about the final graphic object representation, a list of all style parameters and other options for tuning the `show`-output are described in {ref}`examples-graphic-styles` and {ref}`examples-animation`. (intro-field-computation)= ## Magnetic field computation Magnetic field computation in Magpylib is achieved through: - **`getB`**`(sources, observers)` computes the B-field seen by `observers` generated by `sources` in units of \[mT\] - **`getH`**`(sources, observers)` computes the H-field seen by `observers` generated by `sources` in units of \[kA/m\] The argument `sources` can be any Magpylib **source object** or a flat list thereof. The argument `observers` can be an array_like of position vectors with shape $(n_1,n_2,n_3,...,3)$, any Magpylib **observer object** or a flat list thereof. `getB` and `getH` return the field for all combinations of sources, observers and paths. The output of a field computation `getB(sources, observers)` is a Numpy ndarray (alternatively a Pandas DataFrame, see below) of shape `(l, m, k, n1, n2, n3, ..., 3)` where `l` is the number of input sources, `m` the (maximal) object path length, `k` the number of sensors, `n1,n2,n3,...` the sensor pixel shape or the shape of the observer position vector input and `3` the three magnetic field components $(B_x, B_y, B_z)$. **Example 1:** As expressed by the old v2 slogan *"The magnetic field is only three lines of code away"*, this example demonstrates the most fundamental field computation: ```{code-cell} ipython3 import magpylib as magpy loop = magpy.current.Loop(current=1, diameter=2) B = magpy.getB(loop, (1,2,3)) print(B) ``` **Example 2:** When handed with multiple observer positions, `getB` and `getH` will return the field in the shape of the observer input. In the following example, B- and H-field of a cuboid magnet are computed on a position grid, and then displayed using Matplotlib: ```{code-cell} ipython3 import numpy as np import matplotlib.pyplot as plt import magpylib as magpy fig, [ax1,ax2] = plt.subplots(1, 2, figsize=(10,5)) # create an observer grid in the xz-symmetry plane ts = np.linspace(-3, 3, 30) grid = np.array([[(x,0,z) for x in ts] for z in ts]) # compute B- and H-fields of a cuboid magnet on the grid cube = magpy.magnet.Cuboid(magnetization=(500,0,500), dimension=(2,2,2)) B = cube.getB(grid) H = cube.getH(grid) # display field with Pyplot ax1.streamplot(grid[:,:,0], grid[:,:,2], B[:,:,0], B[:,:,2], density=2, color=np.log(np.linalg.norm(B, axis=2)), linewidth=1, cmap='autumn') ax2.streamplot(grid[:,:,0], grid[:,:,2], H[:,:,0], H[:,:,2], density=2, color=np.log(np.linalg.norm(B, axis=2)), linewidth=1, cmap='winter') # outline magnet boundary for ax in [ax1,ax2]: ax.plot([1,1,-1,-1,1], [1,-1,-1,1,1], 'k--') plt.tight_layout() plt.show() ``` **Example 3:** The following example code shows how the field in a position system is computed with a sensor object. Both, magnet and sensor are moving. The 3D system and the field along the path are displayed with Plotly: ```{code-cell} ipython3 import numpy as np import plotly.graph_objects as go import magpylib as magpy # reset defaults set in previous example magpy.defaults.reset() # setup plotly figure and subplots fig = go.Figure().set_subplots(rows=1, cols=2, specs=[[{"type": "scene"}, {"type": "xy"}]]) # define sensor and source sensor = magpy.Sensor(pixel=[(0,0,-.2), (0,0,.2)], style_size=1.5) magnet = magpy.magnet.Cylinder(magnetization=(100,0,0), dimension=(1,2)) # define paths sensor.position = np.linspace((0,0,-3), (0,0,3), 40) magnet.position = (4,0,0) magnet.rotate_from_angax(angle=np.linspace(0, 300, 40)[1:], axis='z', anchor=0) # display system in 3D temp_fig = go.Figure() magpy.show(magnet, sensor, canvas=temp_fig, backend='plotly') fig.add_traces(temp_fig.data, rows=1, cols=1) # compute field and plot B = magpy.getB(magnet, sensor) for i,plab in enumerate(['pixel1', 'pixel2']): for j,lab in enumerate(['_Bx', '_By', '_Bz']): fig.add_trace(go.Scatter(x=np.arange(40), y=B[:,i,j], name=plab+lab)) fig.show() ``` **Example 4:** The last example demonstrates the most general form of a `getB` computation with multiple source and sensor inputs. Specifically, 3 sources, one with path length 11, and two sensors, each with pixel shape (4,5). Note that, when input objects have different path lengths, objects with shorter paths are treated as static beyond their path end. ```{code-cell} ipython3 import magpylib as magpy # 3 sources, one with length 11 path pos_path = [(i,0,1) for i in range(-1,1)] source1 = magpy.misc.Dipole(moment=(0,0,100), position=pos_path) source2 = magpy.current.Loop(current=10, diameter=3) source3 = source1 + source2 # 2 observers, each with 4x5 pixel pixel = [[[(i,j,0)] for i in range(4)] for j in range(5)] sensor1 = magpy.Sensor(pixel=pixel, position=(-1,0,-1)) sensor2 = sensor1.copy().move((2,0,0)) sources = [source1, source2, source3] sensors = [sensor1, sensor2] # compute field B = magpy.getB(sources, sensors) print(B.shape) ``` Instead of a Numpy `ndarray`, the field computation can also return a [pandas](https://pandas.pydata.org/).[dataframe](https://pandas.pydata.org/docs/user_guide/dsintro.html#dataframe) using the `output='dataframe'` kwarg. ```{code-cell} ipython3 import numpy as np import magpylib as magpy cube = magpy.magnet.Cuboid( magnetization=(0, 0, 1000), dimension=(1, 1, 1), style_label='cube' ) loop = magpy.current.Loop( current=200, diameter=2, style_label='loop', ) sens1 = magpy.Sensor( pixel=[(0,0,0), (.5,0,0)], position=np.linspace((-4, 0, 2), (4, 0, 2), 30), style_label='sens1' ) sens2 = sens1.copy(style_label='sens2').move((0,0,1)) B_as_df = magpy.getB( [cube, loop], [sens1, sens2], output='dataframe', ) B_as_df ``` Plotting libraries such as [plotly](https://plotly.com/python/plotly-express/) or [seaborn](https://seaborn.pydata.org/introduction.html) can take advantage of this feature, as they can deal with `dataframes` directly. ```{code-cell} ipython3 import plotly.express as px fig = px.line( B_as_df, x="path", y="Bx", color="pixel", line_group="source", facet_col="source", symbol="sensor", ) fig.show() ``` In terms of **performance** it must be noted that Magpylib automatically vectorizes all computations when `getB` and `getH` are called. This reduces the computation time dramatically for large inputs. For maximal performance try to make all field computations with as few calls to `getB` and `getH` as possible. (intro-direct-interface)= ## Direct interface and core The **direct interface** allows users to bypass the object oriented functionality of Magpylib. The magnetic field is computed for a set of $n$ arbitrary input instances by providing the top level functions `getB` and `getH` with 1. `sources`: a string denoting the source type 2. `observers`: array_like of shape (3,) or (n,3) giving the positions 3. `kwargs`: a dictionary with array_likes of shape (x,) or (n,x) for all other inputs All "scalar" inputs of shape (x,) are automatically tiled up to shape (n,x), and for every of the $n$ given instances the field is computed and returned with shape (n,3). The allowed source types are similar to the Magpylib source class names (see {ref}`intro-magpylib-objects`), and the required dictionary inputs are the respective class inputs. In the following example we compute the cuboid field for 5 input instances, each with different position and orientation and similar magnetization: ```{code-cell} ipython3 import magpylib as magpy B = magpy.getB( sources='Cuboid', observers=[(0,0,x) for x in range(5)], dimension=[(d,d,d) for d in range(1,6)], magnetization=(0,0,1000), ) print(B) ``` The direct interface is convenient for users who work with complex inputs or favor a more functional programming paradigm. It is typically faster than the object oriented interface, but it also requires that users know how to generate the inputs efficiently with numpy (e.g. `np.arange`, `np.linspace`, `np.tile`, `np.repeat`, ...). At the heart of Magpylib lies a set of **core functions** that are our implementations of the analytical field expressions, see {ref}`physcomp`. For users who are not interested in the position/orientation interface, the `magpylib.core` subpackage gives direct access to these functions. Inputs are ndarrays of shape (n,x). Details can be found in the respective function docstrings. ```{code-cell} ipython3 import numpy as np import magpylib as magpy mag = np.array([(100,0,0)]*5) dim = np.array([(1,2,3,45,90)]*5) obs = np.array([(0,0,0)]*5) B = magpy.core.magnet_cylinder_segment_field('B', obs, mag, dim) print(B) ``` (intro-collections)= ## Collections The top level class `Collection` allows users to group objects by reference for common manipulation. Objects that are part of a collection become **children** of that collection, and the collection itself becomes their **parent**. An object can only have a single parent. The child-parent relation is demonstrated with the `describe` method in the following example: ```{code-cell} ipython3 import magpylib as magpy sens = magpy.Sensor(style_label='sens') loop = magpy.current.Loop(style_label='loop') line = magpy.current.Line(style_label='line') cube = magpy.magnet.Cuboid(style_label='cube') coll1 = magpy.Collection(sens, loop, line, style_label='coll1') coll2 = cube + coll1 coll2.describe(format='type,label') ``` A detailed review of collection properties and construction is provided in the example gallery {ref}`examples-collections-construction`. It is specifically noteworthy in the above example, that any two Magpylib objects can simply be added up to form a collection. A collection object has its own `position` and `orientation` attributes and spans a local reference frame for all its children. An operation applied to a collection moves the frame, and is individually applied to all children such that their relative position in the local reference frame is maintained. This means that the collection functions only as a container for manipulation, but child position and orientation are always updated in the global coordinate system. After being added to a collection, it is still possible to manipulate the individual children, which will also move them to a new relative position in the collection frame. This enables user-friendly manipulation of groups, sub-groups and individual objects, which is demonstrated in the following example: ```{code-cell} ipython3 import numpy as np import magpylib as magpy from magpylib.current import Loop # construct two coil collections from windings coil1 = magpy.Collection(style_label='coil1') for z in np.linspace(-.5, .5, 5): coil1.add(Loop(current=1, diameter=20, position=(0,0,z))) coil1.position = (0,0,-5) coil2 = coil1.copy(position=(0,0,5)) # helmholtz consists of two coils helmholtz = coil1 + coil2 # move the helmholz helmholtz.position = np.linspace((0,0,0), (10,0,0), 30) helmholtz.rotate_from_angax(np.linspace(0,180,30), 'x', start=0) # move the coils coil1.move(np.linspace((0,0,0), ( 5,0,0), 30)) coil2.move(np.linspace((0,0,0), (-5,0,0), 30)) # move the windings for coil in [coil1, coil2]: for i,wind in enumerate(coil): wind.move(np.linspace((0,0,0), (0,0,2-i), 20)) magpy.show(*helmholtz, backend='plotly', animation=4, style_path_show=False) ``` Notice, that collections have their own `style` attributes, their paths are displayed in `show`, and all children are automatically assigned their parent color. For magnetic field computation a collection with source children behaves like a single source object, and a collection with sensor children behaves like a flat list of it's sensors when provided as `sources` and `observers` input respectively. This is demonstrated in the following continuation of the previous helmholtz example: ```{code-cell} ipython3 import matplotlib.pyplot as plt B = helmholtz.getB((10,0,0)) plt.plot(B, label=['Bx', 'By', 'Bz']) plt.gca().set( title='B-field [mT] at position (10,0,0)', xlabel='helmholtz path position index' ) plt.gca().grid(color='.9') plt.gca().legend() plt.show() ``` One central motivation behind the `Collection` class is enabling users to build **compound objects**, which refer to custom classes that inherit `Collection`. They can represent complex magnet structures like magnetic encoders, motor parts, halbach arrays, and other arrangments, and will naturally integrate into the Magpylib interface. An advanced tutorial how to sub-class `Collection` with dynamic properties and custom 3D models is given in {ref}`examples-compounds`. (intro-customization)= ## Customization **User-defined 3D models** (traces) for any object that will be displayed by `show`, can be stored in `style.model3d.data`. A trace itself is a dictionary that contains all information necessary for plotting, and can be added with the method `style.model3d.data.add_trace`. In the example gallery {ref}`examples-3d-models` it is explained how to create custom traces with standard plotting backends such as `scatter3d` or `mesh3d` in Plotly, or `plot`, `plot_surface` and `plot_trisurf` in Matplotlib. Some pre-defined models are also provided for easy parts visualization. **User-defined source objects** are easily realized with the `CustomSource` class. Such a custom source object is provided with a user-defined field computation function, that is stored in the attribute `field_func` and is used when `getB` and `getH` are called. Similar to core functions, `field_func` must have the two positional arguments `field` (can be `'B'` or `'H'`) and `observers` (must accept ndarrays of shape (n,3)), and return the respective fields in units of \[mT\] and \[kA/m\] in the same shape. Details on working with custom sources are given in {ref}`examples-custom-source-objects`. While each of these features can be used individually, the combination of the two (own source class with own 3D representation) enables a high level of customization in Magpylib. Such user-defined objects will feel like native Magpylib objects and can be used in combination with all other features, which is demonstrated in the following example: ```{code-cell} ipython3 import numpy as np import plotly.graph_objects as go import magpylib as magpy # define B/H field function for custom source def easter_field(field, observers): """ points in z-direction and decays with 1/r^3""" dist = np.linalg.norm(observers, axis=1) return np.c_[np.zeros((len(observers),2)), 1/dist**3] # create custom source egg = magpy.misc.CustomSource( field_func=easter_field, style=dict(color='orange', model3d_showdefault=False, label='The Egg'), ) # add a custom 3D model trace = magpy.graphics.model3d.make_Ellipsoid( backend='plotly', dimension=(1,1,1.4), ) egg.style.model3d.add_trace(trace) # move the egg ts = np.linspace(-2*np.pi, 2*np.pi, 70) ts = ts + 0.75*np.sin(ts-np.pi/8)**2 egg.position = [(t/3, 0, -0.2*np.sin(t)**2) for t in ts] egg.rotate_from_euler(ts, 'y', start=0, degrees=False) # add sensor and compute field on path sensor = magpy.Sensor(position=(0,0,1.5), style_size=2) B = sensor.getB(egg) # animate path and plot field magpy.show(egg, sensor, backend='plotly', animation=True, style_path_show=False) fig = go.Figure() for i,lab in enumerate(['Bx', 'By', 'Bz']): fig.add_trace(go.Scatter(x=ts/2*3, y=B[:,i], name=lab)) fig.update_layout(title='Field at sensor', xaxis_title='animation time [s]') fig.show() ```