# Magnetic field example

An example mixing numerical caculation and 3D visualization of the magnetic field created by an arbitrary number of current loops.

The goal of this example is to show how Mayavi can be used with scipy to debug and understand physics and electromagnetics computation.

The field is caculated for an arbitrary number of current loops using the corresponding exact formula. The coils are plotted in 3D with a synthetic view of the magnetic_field. A VectorCutPlane is used as it enables good inspection of the magnetic field.

This example originated from a real-life case of coil design in Python ( Atomic sources for long-time-of-flight interferometric inertial sensors, G. Varoquaux, http://tel.archives-ouvertes.fr/tel-00265714/, page 148).

For another visualization of magnetic field, see the Magnetic field lines example.

Python source code: `magnetic_field.py`

    # Author: Gael Varoquaux <gael.varoquaux@normalesup.org>
    # Copyright (c) 2009, Enthought, Inc.
    # License: BSD Style.

    import numpy as np
    from scipy import special
    from mayavi import mlab

    ##############################################################################
    # Function to caculate the Magnetic field generated by a current loop

    def base_vectors(n):
        """ Returns 3 orthognal base vectors, the first one colinear to n.

            Parameters
            -----------
            n: ndarray, shape (3, )
                A vector giving direction of the basis

            Returns
            -----------
            n: ndarray, shape (3, )
                The first vector of the basis
            l: ndarray, shape (3, )
                The second vector of the basis
            m: ndarray, shape (3, )
                The first vector of the basis

        """
        # normalize n
        n = n / (n**2).sum(axis=-1)

        # choose two vectors perpendicular to n
        # choice is arbitrary since the coil is symetric about n
        if  np.abs(n[0])==1 :
            l = np.r_[n[2], 0, -n[0]]
        else:
            l = np.r_[0, n[2], -n[1]]

        l = l / (l**2).sum(axis=-1)
        m = np.cross(n, l)
        return n, l, m

    def magnetic_field(r, n, r0, R):
        """
        Returns the magnetic field from an arbitrary current loop calculated from
        eqns (1) and (2) in Phys Rev A Vol. 35, N 4, pp. 1535-1546; 1987.

        Arguments
        ----------
            n: ndarray, shape (3, )
                The normal vector to the plane of the loop at the center,
                current is oriented by the right-hand-rule.
            r: ndarray, shape (m, 3)
                A position vector where the magnetic field is evaluated:
                [x1 y2 z3 ; x2 y2 z2 ; ... ]
                r is in units of d
            r0: ndarray, shape (3, )
                The location of the center of the loop in units of d: [x y z]
            R: float
                The radius of the current loop

        Returns
        --------
        B: ndarray, shape (m, 3)
            a vector for the B field at each position specified in r
            in inverse units of (mu I) / (2 pi d)
            for I in amps and d in meters and mu = 4 pi * 10^-7 we get Tesla
        """
        ### Translate the coordinates in the coil's frame
        n, l, m = base_vectors(n)

        # transformation matrix coil frame to lab frame
        trans = np.vstack((l, m, n))
        # transformation matrix to lab frame to coil frame
        inv_trans = np.linalg.inv(trans)

        # point location from center of coil
        r = r - r0
        # transform vector to coil frame
        r = np.dot(r, inv_trans)

        #### calculate field

        # express the coordinates in polar form
        x = r[:, 0]
        y = r[:, 1]
        z = r[:, 2]
        rho = np.sqrt(x**2 + y**2)
        theta = np.arctan(x/y)
        theta[y==0] = 0

        E = special.ellipe((4 * R * rho)/( (R + rho)**2 + z**2))
        K = special.ellipk((4 * R * rho)/( (R + rho)**2 + z**2))
        Bz =  1/np.sqrt((R + rho)**2 + z**2) * (
                    K
                  + E * (R**2 - rho**2 - z**2)/((R - rho)**2 + z**2)
                  )
        Brho = z/(rho*np.sqrt((R + rho)**2 + z**2)) * (
                   -K
                  + E * (R**2 + rho**2 + z**2)/((R - rho)**2 + z**2)
                  )
        # On the axis of the coil we get a divided by zero here. This returns a
        # NaN, where the field is actually zero :
        Brho[np.isnan(Brho)] = 0
        Brho[np.isinf(Brho)] = 0
        Bz[np.isnan(Bz)]     = 0
        Bz[np.isinf(Bz)]     = 0

        B = np.c_[np.cos(theta)*Brho, np.sin(theta)*Brho, Bz ]

        # Rotate the field back in the lab's frame
        B = np.dot(B, trans)
        return B

    def display_coil(n, r0, R, half=False):
        """
        Display a coils in the 3D view.
        If half is True, display only one half of the coil.
        """
        n, l, m = base_vectors(n)
        theta = np.linspace(0, (2-half)*np.pi, 30)
        theta = theta[..., np.newaxis]
        coil = np.atleast_1d(R)*(np.sin(theta)*l + np.cos(theta)*m)
        coil += r0
        coil_x = coil[:, 0]
        coil_y = coil[:, 1]
        coil_z = coil[:, 2]
        mlab.plot3d(coil_x, coil_y, coil_z,
                tube_radius=0.01,
                name='Coil %i' % display_coil.num,
                color=(0, 0, 0))
        display_coil.num += 1
        return coil_x, coil_y, coil_z

    display_coil.num = 0

    ##############################################################################
    # The grid of points on which we want to evaluate the field
    X, Y, Z = np.mgrid[-0.15:0.15:31j, -0.15:0.15:31j, -0.15:0.15:31j]
    # Avoid rounding issues :
    f = 1e4  # this gives the precision we are interested by :
    X = np.round(X * f) / f
    Y = np.round(Y * f) / f
    Z = np.round(Z * f) / f

    r = np.c_[X.ravel(), Y.ravel(), Z.ravel()]

    ##############################################################################
    # The coil positions

    # The center of the coil
    r0 = np.r_[0, 0, 0.1]
    # The normal to the coils
    n  = np.r_[0, 0, 1]
    # The radius
    R  = 0.1

    # Add the mirror image of this coils relatively to the xy plane :
    r0 = np.vstack((r0, -r0 ))
    R  = np.r_[R, R]
    n  = np.vstack((n, n))      # Helmoltz like configuration

    ##############################################################################
    # Calculate field
    # First initialize a container matrix for the field vector :
    B = np.empty_like(r)
    # Then loop through the different coils and sum the fields :
    for this_n, this_r0, this_R in zip(n, r0, R):
      this_n    = np.array(this_n)
      this_r0   = np.array(this_r0)
      this_R    = np.array(this_R)
      B += magnetic_field(r, this_n, this_r0, this_R)

    Bx = B[:, 0]
    By = B[:, 1]
    Bz = B[:, 2]
    Bx.shape = X.shape
    By.shape = Y.shape
    Bz.shape = Z.shape

    Bnorm = np.sqrt(Bx**2 + By**2 + Bz**2)

    ##############################################################################
    # Visualization

    # We threshold the data ourselves, as the threshold filter produce a
    # data structure inefficient with IsoSurface
    Bmax = 100

    Bx[Bnorm > Bmax] = 0
    By[Bnorm > Bmax] = 0
    Bz[Bnorm > Bmax] = 0
    Bnorm[Bnorm > Bmax] = Bmax

    mlab.figure(1, bgcolor=(1, 1, 1), fgcolor=(0.5, 0.5, 0.5),
                   size=(480, 480))
    mlab.clf()

    for this_n, this_r0, this_R in zip(n, r0, R):
      display_coil(this_n, this_r0, this_R)

    field = mlab.pipeline.vector_field(X, Y, Z, Bx, By, Bz,
                                      scalars=Bnorm, name='B field')
    vectors = mlab.pipeline.vectors(field,
                          scale_factor=(X[1, 0, 0] - X[0, 0, 0]),
                          )
    # Mask random points, to have a lighter visualization.
    vectors.glyph.mask_input_points = True
    vectors.glyph.mask_points.on_ratio = 6

    vcp = mlab.pipeline.vector_cut_plane(field)
    vcp.glyph.glyph.scale_factor=5*(X[1, 0, 0] - X[0, 0, 0])
    # For prettier picture:
    #vcp.implicit_plane.widget.enabled = False

    iso = mlab.pipeline.iso_surface(field,
                                    contours=[0.1*Bmax, 0.4*Bmax],
                                    opacity=0.6,
                                    colormap='YlOrRd')

    # A trick to make transparency look better: cull the front face
    iso.actor.property.frontface_culling = True

    mlab.view(39, 74, 0.59, [.008, .0007, -.005])

    mlab.show()