Skip to content

Magnets module

pymagnet.magnets

This module imports the classes and functions in the private modules to create a public API.

Circle

Bases: Magnet2D

Circle 2D Magnet Class

Source code in src/pymagnet/magnets/_magnet2D.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
class Circle(Magnet2D):
    """Circle 2D Magnet Class"""

    mag_type = "Circle"

    def __init__(
        self,
        radius=10,
        Jr=1.0,  # local magnetisation
        **kwargs,
    ):
        """Init Method

        Args:
            radius (float, optional): Radius. Defaults to 10.0.
            Jr (float, optional): Remnant magnetisation. Defaults to 1.0.

        Kwargs:
            alpha (float): Unused. For rotations use phi instead
            center (tuple or ndarray): magnet center (x, y). Defaults to (0,0)
            phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
        """
        super().__init__(Jr, **kwargs)
        self.radius = radius
        self.phi = kwargs.pop("phi", 0)
        self.phi_rad = _np.deg2rad(self.phi)

        self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
        self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
        self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

        self.center = kwargs.pop("center", _np.array([0.0, 0.0]))
        self.center = _np.asarray(self.center)

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation: alpha {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation: alpha {self.get_orientation()}\n"
        )
        return str

    def get_size(self):
        """Returns radius

        Returns:
            ndarray: radius
        """
        return _np.array([self.radius])

    def get_Jr(self):
        """Returns signed remnant magnetisation

        Returns:
            ndarray: remnant magnetisation
        """
        return _np.array([self.Jx, self.Jy])

    def get_field(self, x, y):
        """Calculates the magnetic field due to long bipolar cylinder

        Args:
            x (ndarray): x coordinates
            y (ndarray): y coordinates

        Returns:
            tuple: Bx, By magnetic field in cartesian coordinates
        """
        from ..utils._conversions import cart2pol, vector_pol2cart
        from ..utils._routines2D import rotate_points_2D

        if _np.fabs(self.alpha_radians) > Magnet2D.tol:
            xi, yi = rotate_points_2D(
                x - self.center[0], y - self.center[1], self.alpha_radians
            )

            rho, phi = cart2pol(xi, yi)
            Brho, Bphi = self._calcB_polar(rho, phi - self.phi_rad)

            # Convert magnetic fields from cylindrical to cartesian
            Bx, By = vector_pol2cart(Brho, Bphi, phi)
            Bx, By = rotate_points_2D(Bx, By, 2 * PI - self.alpha_radians)
            return Bx, By

        rho, phi = cart2pol(x - self.center[0], y - self.center[1])

        Brho, Bphi = self._calcB_polar(rho, phi - self.phi_rad)

        # Convert magnetic fields from cylindrical to cartesian
        Bx, By = vector_pol2cart(Brho, Bphi, phi)

        return Bx, By

    def _calcB_polar(self, rho, phi):
        """Calculates the magnetic field due to long bipolar cylinder in polar
        coordinates

        Args:
            rho (ndarray): radial values
            phi (ndarray): azimuthal values

        Returns:
            tuple: Br, Bphi magnetic field in polar coordinates
        """
        prefac = self.Jr * (self.radius**2 / rho**2) / 2

        Brho = prefac * _np.cos(phi)
        Bphi = prefac * _np.sin(phi)

        return Brho, Bphi

__init__(radius=10, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
radius float

Radius. Defaults to 10.0.

10
Jr float

Remnant magnetisation. Defaults to 1.0.

1.0
Kwargs

alpha (float): Unused. For rotations use phi instead center (tuple or ndarray): magnet center (x, y). Defaults to (0,0) phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.

Source code in src/pymagnet/magnets/_magnet2D.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def __init__(
    self,
    radius=10,
    Jr=1.0,  # local magnetisation
    **kwargs,
):
    """Init Method

    Args:
        radius (float, optional): Radius. Defaults to 10.0.
        Jr (float, optional): Remnant magnetisation. Defaults to 1.0.

    Kwargs:
        alpha (float): Unused. For rotations use phi instead
        center (tuple or ndarray): magnet center (x, y). Defaults to (0,0)
        phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
    """
    super().__init__(Jr, **kwargs)
    self.radius = radius
    self.phi = kwargs.pop("phi", 0)
    self.phi_rad = _np.deg2rad(self.phi)

    self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
    self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
    self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

    self.center = kwargs.pop("center", _np.array([0.0, 0.0]))
    self.center = _np.asarray(self.center)

get_Jr()

Returns signed remnant magnetisation

Returns:

Type Description
ndarray

remnant magnetisation

Source code in src/pymagnet/magnets/_magnet2D.py
360
361
362
363
364
365
366
def get_Jr(self):
    """Returns signed remnant magnetisation

    Returns:
        ndarray: remnant magnetisation
    """
    return _np.array([self.Jx, self.Jy])

get_field(x, y)

Calculates the magnetic field due to long bipolar cylinder

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required

Returns:

Type Description
tuple

Bx, By magnetic field in cartesian coordinates

Source code in src/pymagnet/magnets/_magnet2D.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def get_field(self, x, y):
    """Calculates the magnetic field due to long bipolar cylinder

    Args:
        x (ndarray): x coordinates
        y (ndarray): y coordinates

    Returns:
        tuple: Bx, By magnetic field in cartesian coordinates
    """
    from ..utils._conversions import cart2pol, vector_pol2cart
    from ..utils._routines2D import rotate_points_2D

    if _np.fabs(self.alpha_radians) > Magnet2D.tol:
        xi, yi = rotate_points_2D(
            x - self.center[0], y - self.center[1], self.alpha_radians
        )

        rho, phi = cart2pol(xi, yi)
        Brho, Bphi = self._calcB_polar(rho, phi - self.phi_rad)

        # Convert magnetic fields from cylindrical to cartesian
        Bx, By = vector_pol2cart(Brho, Bphi, phi)
        Bx, By = rotate_points_2D(Bx, By, 2 * PI - self.alpha_radians)
        return Bx, By

    rho, phi = cart2pol(x - self.center[0], y - self.center[1])

    Brho, Bphi = self._calcB_polar(rho, phi - self.phi_rad)

    # Convert magnetic fields from cylindrical to cartesian
    Bx, By = vector_pol2cart(Brho, Bphi, phi)

    return Bx, By

get_size()

Returns radius

Returns:

Type Description
ndarray

radius

Source code in src/pymagnet/magnets/_magnet2D.py
352
353
354
355
356
357
358
def get_size(self):
    """Returns radius

    Returns:
        ndarray: radius
    """
    return _np.array([self.radius])

Cube

Bases: Prism

Cube 3D Magnet Class

Source code in src/pymagnet/magnets/_magnet3D.py
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
class Cube(Prism):
    """Cube 3D Magnet Class"""

    mag_type = "Cube"

    def __init__(
        self,
        width=10.0,  # magnet dimensions
        Jr=1.0,  # local magnetisation direction
        **kwargs,
    ):
        """Init method

        Args:
            width (float, optional): Cube side length. Defaults to 10.0.

        Kwargs:
            center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
            mask_magnet (bool): Flag to mask magnet or not in plots
            alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0
            beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0
            gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0
            phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0
            theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0
        """

        super().__init__(width=width, depth=width, height=width, Jr=Jr, **kwargs)

__init__(width=10.0, Jr=1.0, **kwargs)

Init method

Parameters:

Name Type Description Default
width float

Cube side length. Defaults to 10.0.

10.0
Kwargs

center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0) mask_magnet (bool): Flag to mask magnet or not in plots alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0 beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0 gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0 phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0 theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0

Source code in src/pymagnet/magnets/_magnet3D.py
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
def __init__(
    self,
    width=10.0,  # magnet dimensions
    Jr=1.0,  # local magnetisation direction
    **kwargs,
):
    """Init method

    Args:
        width (float, optional): Cube side length. Defaults to 10.0.

    Kwargs:
        center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
        mask_magnet (bool): Flag to mask magnet or not in plots
        alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0
        beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0
        gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0
        phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0
        theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0
    """

    super().__init__(width=width, depth=width, height=width, Jr=Jr, **kwargs)

Cylinder

Bases: Magnet3D

Cylinder 3D Magnet Class

Returns:

Type Description
Cylinder

Cylinder 3D magnet object

Source code in src/pymagnet/magnets/_magnet3D.py
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
class Cylinder(Magnet3D):
    """Cylinder 3D Magnet Class

    Returns:
        Cylinder: Cylinder 3D magnet object
    """

    mag_type = "Cylinder"

    def __init__(
        self,
        radius=10.0,
        length=10.0,  # magnet dimensions
        Jr=1.0,  # local magnetisation direction
        **kwargs,
    ):
        """Init Method

        Args:
            radius (float, optional): radius. Defaults to 10.0.
            length (float, optional): length. Defaults to 10.0.

        Kwargs:
            center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
            mask_magnet (bool): Flag to mask magnet or not in plots
            alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0
            beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0
            gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0
            phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0
            theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0
        """
        super().__init__(Jr, **kwargs)
        self.radius = radius
        self.length = length

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.Jr} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def get_size(self):
        """Returns magnet dimesions

        Returns:
            size[ndarray]: numpy array [radius, length]
        """
        return _np.array([self.radius, self.length])

    def get_Jr(self):
        return _np.array([0.0, 0.0, self.Jr])

    def get_force_torque(self, num_samples=20, unit="mm"):
        """Calculates the force and torque on a cylinder magnet due to all other magnets.

        Args:
            num_samples (int, optional): Number of samples per axis per face. Defaults to 20.
            unit (str, optional): Length scale. Defaults to 'mm'.

        Returns:
            tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
        """
        from ..forces._cylinder_force import calc_force_cylinder

        force, torque = calc_force_cylinder(self, num_samples, unit)
        return force, torque

    def _get_field_internal(self, x, y, z):
        """Internal magnetic field calculation methods.
        Calculates the field due to a cylindrical magnet/solenoid magnetised along z
        (in local coordinates).

        Args:
            x (array): x co-ordinates
            y (array): y co-ordinates
            z (array): z co-ordinates

        Returns:
            Field: Magnetic field array
        """
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)

        rho = _np.sqrt(x * x + y * y)

        Brho, B.z = self._calcB_cyl(rho, z)

        # Inline pol2cart without arctan2/cos/sin:
        #   Bx = Brho * cos(phi) = Brho * x/rho
        #   By = Brho * sin(phi) = Brho * y/rho
        # On the z-axis rho=0 and Brho=0 by symmetry; use safe reciprocal to
        # avoid 0/0 producing NaN.
        inv_rho = _np.where(rho > 0.0, 1.0 / rho, 0.0)
        B.x = Brho * x * inv_rho
        B.y = Brho * y * inv_rho

        return B

    def _calcB_cyl(self, rho, z):
        """Calculates the magnetic field due to a solenoid/cylinder in
        polar cylindrical coordinates

        Args:
            rho (array): radial coordinates
            z (array): axial coordinates

        Returns:
            tuple: Brho (ndarray), Bz (ndarray) magnetic field components
        """
        a = self.radius
        b = self.length / 2
        B0 = self.Jr / PI

        zp = z + b
        zn = z - b

        zp_sq = _np.power(zp, 2)
        zn_sq = _np.power(zn, 2)
        rho_a_sq = _np.power(rho + a, 2)
        nrho_a_sq = _np.power(a - rho, 2)

        alphap = a / _np.sqrt(zp_sq + rho_a_sq)
        alphan = a / _np.sqrt(zn_sq + rho_a_sq)

        betap = zp / _np.sqrt(zp_sq + rho_a_sq)
        betan = zn / _np.sqrt(zn_sq + rho_a_sq)

        gamma = (a - rho) / (a + rho)

        kp = _np.sqrt((zp_sq + nrho_a_sq) / (zp_sq + rho_a_sq))

        kn = _np.sqrt((zn_sq + nrho_a_sq) / (zn_sq + rho_a_sq))

        Brho = B0 * (alphap * _cel(kp, 1, 1, -1) - alphan * _cel(kn, 1, 1, -1))

        Bz = (B0 * a / (a + rho)) * (
            betap * _cel(kp, gamma**2, 1, gamma) - betan * _cel(kn, gamma**2, 1, gamma)
        )
        return Brho, Bz

    def _generate_mask(self, x, y, z):
        """Generates mask of points inside a cylindrical magnet

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates
        """

        radius, length = self.get_size()
        data_norm = x**2 + y**2
        zn = -length / 2
        zp = length / 2

        mask_rho = data_norm < radius**2
        mask_z = _np.logical_and(z > zn, z < zp)
        mask = _np.logical_and(mask_rho, mask_z)

        return mask

__init__(radius=10.0, length=10.0, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
radius float

radius. Defaults to 10.0.

10.0
length float

length. Defaults to 10.0.

10.0
Kwargs

center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0) mask_magnet (bool): Flag to mask magnet or not in plots alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0 beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0 gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0 phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0 theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0

Source code in src/pymagnet/magnets/_magnet3D.py
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
def __init__(
    self,
    radius=10.0,
    length=10.0,  # magnet dimensions
    Jr=1.0,  # local magnetisation direction
    **kwargs,
):
    """Init Method

    Args:
        radius (float, optional): radius. Defaults to 10.0.
        length (float, optional): length. Defaults to 10.0.

    Kwargs:
        center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
        mask_magnet (bool): Flag to mask magnet or not in plots
        alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0
        beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0
        gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0
        phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0
        theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0
    """
    super().__init__(Jr, **kwargs)
    self.radius = radius
    self.length = length

get_force_torque(num_samples=20, unit='mm')

Calculates the force and torque on a cylinder magnet due to all other magnets.

Parameters:

Name Type Description Default
num_samples int

Number of samples per axis per face. Defaults to 20.

20
unit str

Length scale. Defaults to 'mm'.

'mm'

Returns:

Type Description
tuple

force (ndarray (3,) ) and torque (ndarray (3,) )

Source code in src/pymagnet/magnets/_magnet3D.py
733
734
735
736
737
738
739
740
741
742
743
744
745
746
def get_force_torque(self, num_samples=20, unit="mm"):
    """Calculates the force and torque on a cylinder magnet due to all other magnets.

    Args:
        num_samples (int, optional): Number of samples per axis per face. Defaults to 20.
        unit (str, optional): Length scale. Defaults to 'mm'.

    Returns:
        tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
    """
    from ..forces._cylinder_force import calc_force_cylinder

    force, torque = calc_force_cylinder(self, num_samples, unit)
    return force, torque

get_size()

Returns magnet dimesions

Returns:

Type Description
size[ndarray]

numpy array [radius, length]

Source code in src/pymagnet/magnets/_magnet3D.py
722
723
724
725
726
727
728
def get_size(self):
    """Returns magnet dimesions

    Returns:
        size[ndarray]: numpy array [radius, length]
    """
    return _np.array([self.radius, self.length])

Line

Line Class for storing properties of a sheet manget

Source code in src/pymagnet/magnets/_polygon2D.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
class Line:
    """Line Class for storing properties of a sheet manget"""

    def __init__(self, length, center, beta, K):
        """Init Method

        Args:
            length (float): side length
            center (ndarray): magnet center (x, y)
            beta (float): Orientation w.r.t. z-axis in degrees
            K (float): Sheet current density in tesla
        """
        self.length = length
        self.center = center
        self.beta = beta
        self.beta_rad = _np.deg2rad(beta)
        self.xc = center[0]
        self.yc = center[1]
        self.K = K
        self.tol = MAG_TOL

    def __str__(self):
        str = (
            f"K: {self.K} (T)\n"
            + f"Length: {self.length} (m)\n"
            + f"Center {self.center} (m)\n"
            + f"Orientation: {self.beta}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"K: {self.K} (T)\n"
            + f"Length: {self.length}\n"
            + f"Center {self.center}\n"
            + f"Orientation: {self.beta}\n"
        )
        return str

    def get_center(self):
        """Returns line center

        Returns:
            ndarray: center (x,y)
        """

        return self.center

    def get_field(self, x, y):
        """Calculates the magnetic field due to a sheet magnet
        First a transformation into the local coordinates is made, the field calculated
        and then the magnetic field it rotated out to the global coordinates

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            tuple: Bx (ndarray), By (ndarray) magnetic field vector
        """
        from ..utils._routines2D import _get_field_array_shape2, rotate_points_2D

        array_shape = _get_field_array_shape2(x, y)
        Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)
        if _np.fabs(self.beta_rad) > self.tol:
            xt, yt = rotate_points_2D(x - self.xc, y - self.yc, 2 * PI - self.beta_rad)
            Btx, Bty = _sheet_field(xt, yt, self.length / 2, self.K)
            Bx, By = rotate_points_2D(Btx, Bty, self.beta_rad)

        else:
            Bx, By = _sheet_field(x - self.xc, y - self.yc, self.length / 2, self.K)
        return Bx, By

__init__(length, center, beta, K)

Init Method

Parameters:

Name Type Description Default
length float

side length

required
center ndarray

magnet center (x, y)

required
beta float

Orientation w.r.t. z-axis in degrees

required
K float

Sheet current density in tesla

required
Source code in src/pymagnet/magnets/_polygon2D.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def __init__(self, length, center, beta, K):
    """Init Method

    Args:
        length (float): side length
        center (ndarray): magnet center (x, y)
        beta (float): Orientation w.r.t. z-axis in degrees
        K (float): Sheet current density in tesla
    """
    self.length = length
    self.center = center
    self.beta = beta
    self.beta_rad = _np.deg2rad(beta)
    self.xc = center[0]
    self.yc = center[1]
    self.K = K
    self.tol = MAG_TOL

get_center()

Returns line center

Returns:

Type Description
ndarray

center (x,y)

Source code in src/pymagnet/magnets/_polygon2D.py
299
300
301
302
303
304
305
306
def get_center(self):
    """Returns line center

    Returns:
        ndarray: center (x,y)
    """

    return self.center

get_field(x, y)

Calculates the magnetic field due to a sheet magnet First a transformation into the local coordinates is made, the field calculated and then the magnetic field it rotated out to the global coordinates

Parameters:

Name Type Description Default
x ndarray

x-coordinates

required
y ndarray

y-coordinates

required

Returns:

Type Description
tuple

Bx (ndarray), By (ndarray) magnetic field vector

Source code in src/pymagnet/magnets/_polygon2D.py
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def get_field(self, x, y):
    """Calculates the magnetic field due to a sheet magnet
    First a transformation into the local coordinates is made, the field calculated
    and then the magnetic field it rotated out to the global coordinates

    Args:
        x (ndarray): x-coordinates
        y (ndarray): y-coordinates

    Returns:
        tuple: Bx (ndarray), By (ndarray) magnetic field vector
    """
    from ..utils._routines2D import _get_field_array_shape2, rotate_points_2D

    array_shape = _get_field_array_shape2(x, y)
    Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)
    if _np.fabs(self.beta_rad) > self.tol:
        xt, yt = rotate_points_2D(x - self.xc, y - self.yc, 2 * PI - self.beta_rad)
        Btx, Bty = _sheet_field(xt, yt, self.length / 2, self.K)
        Bx, By = rotate_points_2D(Btx, Bty, self.beta_rad)

    else:
        Bx, By = _sheet_field(x - self.xc, y - self.yc, self.length / 2, self.K)
    return Bx, By

LineUtils

Utility class consisting of rountines for 2D line elements

Source code in src/pymagnet/magnets/_polygon2D.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
class LineUtils:
    """Utility class consisting of rountines for 2D line elements"""

    @staticmethod
    def unit_norm(vertex_1, vertex_2, clockwise=True):
        """Get unit normal to vertex

        Args:
            vertex_1 (ndarray): vertex 1
            vertex_2 (ndarray): vertex 2
            clockwise (bool, optional): Clockwise orientation of points.
                Defaults to True.

        Returns:
            tuple: normal vector (ndarray), length i.e. distance between
                vertices (float)
        """

        dx = vertex_1[0] - vertex_2[0]
        dy = vertex_1[1] - vertex_2[1]

        # Clockwise winding of points:
        if clockwise:
            norm = _np.array([dy, -dx])
        else:
            norm = _np.array([-dy, dx])
        length = _np.linalg.norm(norm)
        norm = norm / length
        return norm, length

    @staticmethod
    def line_center(vertex_1, vertex_2):
        """Gets midpoint of two vertices

        Args:
            vertex_1 (ndarray): vertex 1
            vertex_2 (ndarray): vertex 2

        Returns:
            ndarray: midpoint
        """
        xc = (vertex_1[0] + vertex_2[0]) / 2
        yc = (vertex_1[1] + vertex_2[1]) / 2

        return _np.array([xc, yc])

    @staticmethod
    def signed_area2D(polygon):
        """Calculates signed area of a polygon

        Args:
            polygon (Polygon): Polygon instance

        Returns:
            float: signed area
        """
        j = 1
        NP = polygon.num_vertices()
        area = 0
        norm = _np.empty([NP, 2])
        center = _np.empty([NP, 2])
        beta = _np.empty(NP)  # angle w.r.t. y axis
        length = _np.empty(NP)

        for i in range(NP):
            j = j % NP
            area += (polygon.vertices[j][0] - polygon.vertices[i][0]) * (
                polygon.vertices[j][1] + polygon.vertices[i][1]
            )
            norm[i, :], length[i] = LineUtils.unit_norm(
                polygon.vertices[i], polygon.vertices[j]
            )
            center[i, :] = LineUtils.line_center(
                polygon.vertices[i], polygon.vertices[j]
            )
            j += 1

        # check winding order of polygon, area < 0 for clockwise ordering of points
        if area < 0:
            norm *= -1
        beta[:] = _np.rad2deg(_np.arctan2(norm[:, 1], norm[:, 0]))

        return area / 2.0, norm, beta, length, center

line_center(vertex_1, vertex_2) staticmethod

Gets midpoint of two vertices

Parameters:

Name Type Description Default
vertex_1 ndarray

vertex 1

required
vertex_2 ndarray

vertex 2

required

Returns:

Type Description
ndarray

midpoint

Source code in src/pymagnet/magnets/_polygon2D.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
@staticmethod
def line_center(vertex_1, vertex_2):
    """Gets midpoint of two vertices

    Args:
        vertex_1 (ndarray): vertex 1
        vertex_2 (ndarray): vertex 2

    Returns:
        ndarray: midpoint
    """
    xc = (vertex_1[0] + vertex_2[0]) / 2
    yc = (vertex_1[1] + vertex_2[1]) / 2

    return _np.array([xc, yc])

signed_area2D(polygon) staticmethod

Calculates signed area of a polygon

Parameters:

Name Type Description Default
polygon Polygon

Polygon instance

required

Returns:

Type Description
float

signed area

Source code in src/pymagnet/magnets/_polygon2D.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
@staticmethod
def signed_area2D(polygon):
    """Calculates signed area of a polygon

    Args:
        polygon (Polygon): Polygon instance

    Returns:
        float: signed area
    """
    j = 1
    NP = polygon.num_vertices()
    area = 0
    norm = _np.empty([NP, 2])
    center = _np.empty([NP, 2])
    beta = _np.empty(NP)  # angle w.r.t. y axis
    length = _np.empty(NP)

    for i in range(NP):
        j = j % NP
        area += (polygon.vertices[j][0] - polygon.vertices[i][0]) * (
            polygon.vertices[j][1] + polygon.vertices[i][1]
        )
        norm[i, :], length[i] = LineUtils.unit_norm(
            polygon.vertices[i], polygon.vertices[j]
        )
        center[i, :] = LineUtils.line_center(
            polygon.vertices[i], polygon.vertices[j]
        )
        j += 1

    # check winding order of polygon, area < 0 for clockwise ordering of points
    if area < 0:
        norm *= -1
    beta[:] = _np.rad2deg(_np.arctan2(norm[:, 1], norm[:, 0]))

    return area / 2.0, norm, beta, length, center

unit_norm(vertex_1, vertex_2, clockwise=True) staticmethod

Get unit normal to vertex

Parameters:

Name Type Description Default
vertex_1 ndarray

vertex 1

required
vertex_2 ndarray

vertex 2

required
clockwise bool

Clockwise orientation of points. Defaults to True.

True

Returns:

Type Description
tuple

normal vector (ndarray), length i.e. distance between vertices (float)

Source code in src/pymagnet/magnets/_polygon2D.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@staticmethod
def unit_norm(vertex_1, vertex_2, clockwise=True):
    """Get unit normal to vertex

    Args:
        vertex_1 (ndarray): vertex 1
        vertex_2 (ndarray): vertex 2
        clockwise (bool, optional): Clockwise orientation of points.
            Defaults to True.

    Returns:
        tuple: normal vector (ndarray), length i.e. distance between
            vertices (float)
    """

    dx = vertex_1[0] - vertex_2[0]
    dy = vertex_1[1] - vertex_2[1]

    # Clockwise winding of points:
    if clockwise:
        norm = _np.array([dy, -dx])
    else:
        norm = _np.array([-dy, dx])
    length = _np.linalg.norm(norm)
    norm = norm / length
    return norm, length

Magnet

Bases: Registry

Magnet base class

Returns:

Type Description
Magnet

magnet base class

Source code in src/pymagnet/magnets/_magnet_base.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class Magnet(Registry):
    """Magnet base class


    Returns:
        Magnet: magnet base class
    """

    tol = MAG_TOL  # tolerance for rotations, sufficient for 0.01 degree accuracy
    mag_type = "Magnet"

    def __init__(self, *args, **kwargs):
        super().__init__()
        self.center = _np.array([0.0, 0.0])

Magnet2D

Bases: Magnet

2D Magnet Base Class

Source code in src/pymagnet/magnets/_magnet2D.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class Magnet2D(Magnet):
    """2D Magnet Base Class"""

    mag_type = "Magnet2D"

    def __init__(self, Jr, **kwargs) -> None:
        """Init Method

        Args:
            Jr (float): signed magnetised of remnant magnetisation

        Kwargs:
            alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
            center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
        """
        super().__init__()
        self.Jr = Jr

        # Magnet rotation w.r.t. x-axis
        self.alpha = kwargs.pop("alpha", 0.0)
        self.alpha_radians = _np.deg2rad(self.alpha)

        self.center = kwargs.pop("center", _np.array([0.0, 0.0]))
        self.center = _np.asarray(self.center)

    def get_center(self):
        """Returns magnet centre

        Returns:
            center (ndarray): numpy array [xc, yc]
        """
        return self.center

    def get_orientation(self):
        """Returns magnet orientation, `alpha` in degrees

        Returns:
            float: alpha, rotation angle w.r.t x-axis.
        """

        return self.alpha

    def get_field(self, x, y) -> None:
        """Calculates the magnetic field.

        This is a template that needs to be implemented for each magnet

        Args:
            x (ndarray): x co-ordinates
            y (ndarray): y co-ordinates
        """
        pass

    def get_force_torque(self) -> None:
        """Calculates the force and torque on a magnet due to all other magnets.

        This is a template that needs to be implemented for each magnet.
        """
        pass

__init__(Jr, **kwargs)

Init Method

Parameters:

Name Type Description Default
Jr float

signed magnetised of remnant magnetisation

required
Kwargs

alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0. center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).

Source code in src/pymagnet/magnets/_magnet2D.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(self, Jr, **kwargs) -> None:
    """Init Method

    Args:
        Jr (float): signed magnetised of remnant magnetisation

    Kwargs:
        alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
        center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
    """
    super().__init__()
    self.Jr = Jr

    # Magnet rotation w.r.t. x-axis
    self.alpha = kwargs.pop("alpha", 0.0)
    self.alpha_radians = _np.deg2rad(self.alpha)

    self.center = kwargs.pop("center", _np.array([0.0, 0.0]))
    self.center = _np.asarray(self.center)

get_center()

Returns magnet centre

Returns:

Type Description
center (ndarray

numpy array [xc, yc]

Source code in src/pymagnet/magnets/_magnet2D.py
44
45
46
47
48
49
50
def get_center(self):
    """Returns magnet centre

    Returns:
        center (ndarray): numpy array [xc, yc]
    """
    return self.center

get_field(x, y)

Calculates the magnetic field.

This is a template that needs to be implemented for each magnet

Parameters:

Name Type Description Default
x ndarray

x co-ordinates

required
y ndarray

y co-ordinates

required
Source code in src/pymagnet/magnets/_magnet2D.py
61
62
63
64
65
66
67
68
69
70
def get_field(self, x, y) -> None:
    """Calculates the magnetic field.

    This is a template that needs to be implemented for each magnet

    Args:
        x (ndarray): x co-ordinates
        y (ndarray): y co-ordinates
    """
    pass

get_force_torque()

Calculates the force and torque on a magnet due to all other magnets.

This is a template that needs to be implemented for each magnet.

Source code in src/pymagnet/magnets/_magnet2D.py
72
73
74
75
76
77
def get_force_torque(self) -> None:
    """Calculates the force and torque on a magnet due to all other magnets.

    This is a template that needs to be implemented for each magnet.
    """
    pass

get_orientation()

Returns magnet orientation, alpha in degrees

Returns:

Type Description
float

alpha, rotation angle w.r.t x-axis.

Source code in src/pymagnet/magnets/_magnet2D.py
52
53
54
55
56
57
58
59
def get_orientation(self):
    """Returns magnet orientation, `alpha` in degrees

    Returns:
        float: alpha, rotation angle w.r.t x-axis.
    """

    return self.alpha

Magnet3D

Bases: Magnet

3D Magnet Base Class

Returns:

Type Description
Magnet3D

3D magnet object

Source code in src/pymagnet/magnets/_magnet3D.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class Magnet3D(Magnet):
    """3D Magnet Base Class

    Returns:
        Magnet3D: 3D magnet object
    """

    mag_type = "Magnet3D"

    def __init__(self, Jr, **kwargs) -> None:
        """Init Method

        Args:
            Jr (float): Signed remnant magnetisation

        Kwargs:
            center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
            mask_magnet (bool): Flag to mask magnet or not in plots
            alpha (float): Magnet Orientation angle about z (degrees).
            Defaults to 0.0
            beta (float): Magnet Orientation angle about y (degrees).
            Defaults to 0.0
            gamma (float): Magnet Orientation angle about x (degrees).
            Defaults to 0.0
        """
        super().__init__()

        self.Jr = Jr

        self.center = kwargs.pop("center", _np.array([0.0, 0.0, 0.0]))
        self.center = _np.asarray(self.center)

        self._mask_magnet = kwargs.pop("mask_magnet", False)

        # if type(self.center) is tuple:
        #     center = Point3(self.center[0], self.center[1], self.center[2])
        # self.xc = self.center[0]
        # self.yc = self.center[1]
        # self.zc = self.center[2]

        self.alpha = kwargs.pop("alpha", 0.0)  # rotation angle about z
        self.beta = kwargs.pop("beta", 0.0)  # rotation angle about y
        self.gamma = kwargs.pop("gamma", 0.0)  # rotation angle about x

        self.alpha_rad = _np.deg2rad(self.alpha)
        self.beta_rad = _np.deg2rad(self.beta)
        self.gamma_rad = _np.deg2rad(self.gamma)

    def get_center(self):
        """Returns magnet center

        Returns:
            ndarray: [center_x, center_y, center_z]
        """
        return self.center

    def get_Jr(self):
        """Returns local magnetisation orientation

        Must be implemented for all classes
        """
        pass

    def get_orientation(self):
        """Returns magnet orientation, `alpha`, `beta`, `gamma` in degrees

        Returns:
            ndarray: alpha, beta, gamma rotation angles w.r.t z, y, and x axes
        """

        return _np.array([self.alpha, self.beta, self.gamma])

    def _generate_rotation_quaternions(self):
        """Generates single rotation quaternion for all non-zero rotation angles,
        which are:

            alpha: angle in degrees around z-axis
            beta: angle in degrees around y-axis
            gamma: angle in degrees around x-axis

        Returns:
            Quaternion: total rotation quaternion
        """

        # Initialise quaternions
        rotate_about_x = Quaternion()
        rotate_about_y = Quaternion()
        rotate_about_z = Quaternion()

        forward_rotation, reverse_rotation = Quaternion(), Quaternion()

        if _np.fabs(self.alpha_rad) > 1e-4:
            rotate_about_z = q_angle_from_axis(self.alpha_rad, (0, 0, 1))

        if _np.fabs(self.beta_rad) > 1e-4:
            rotate_about_y = q_angle_from_axis(self.beta_rad, (0, 1, 0))

        if _np.fabs(self.gamma_rad) > 1e-4:
            rotate_about_x = q_angle_from_axis(self.gamma_rad, (1, 0, 0))

        # Generate compound rotations
        # Order of rotation: beta  about y, alpha about z, gamma about x
        forward_rotation = rotate_about_x * rotate_about_z * rotate_about_y  # type: ignore

        reverse_rotation = forward_rotation.get_conjugate()  # type: ignore

        return forward_rotation, reverse_rotation

    def get_field(self, x, y, z):
        """Calculates the magnetic field at point(s) x,y,z due to a 3D magnet
        The calculations are always performed in local coordinates with the
        centre of the magnet at origin and z magnetisation pointing along the
        local z' axis.

        The rotations and translations are performed first, and the internal
        field calculation functions are called.

        Args:
            x (ndarray): x co-ordinates
            y (ndarray): y co-ordinates
            z (ndarray): z co-ordinates

        Returns:
            tuple: Bx(ndarray), By(ndarray), Bz(ndarray) field vector
        """
        from ..utils._routines3D import _apply_mask, _tile_arrays

        # If any rotation angle is set, transform the data
        if _np.any(
            _np.fabs(
                _np.array(
                    [
                        self.alpha_rad,
                        self.beta_rad,
                        self.gamma_rad,
                    ]
                )
            )
            > Magnet.tol
        ):
            forward_rotation, reverse_rotation = self._generate_rotation_quaternions()
            assert forward_rotation is not None
            assert reverse_rotation is not None
            # Generate 3xN array for quaternion rotation
            pos_vec = Quaternion._prepare_vector(
                x - self.center[0], y - self.center[1], z - self.center[2]
            )
            assert pos_vec is not None
            # Rotate points
            x_rot, y_rot, z_rot = forward_rotation * pos_vec  # type: ignore

            # Calls internal child method to calculate the field
            B_local = self._get_field_internal(x_rot, y_rot, z_rot)
            mask = self._generate_mask(x_rot, y_rot, z_rot)

            B_local = _apply_mask(self, B_local, mask)

            # Rearrange the field vectors in a 3xN array for quaternion rotation
            Bvec = Quaternion._prepare_vector(B_local.x, B_local.y, B_local.z)

            # Rotate the local fields back into the global frame using quaternions
            Bx, By, Bz = reverse_rotation * Bvec

            # finally return the fields
            return Bx, By, Bz

        else:
            # Otherwise directly calculate the magnetic fields
            B = self._get_field_internal(
                x - self.center[0], y - self.center[1], z - self.center[2]
            )

            xloc, yloc, zloc = _tile_arrays(
                x - self.center[0], y - self.center[1], z - self.center[2]
            )
            mask = self._generate_mask(xloc, yloc, zloc)
            B = _apply_mask(self, B, mask)

            return B.x, B.y, B.z

    def get_force_torque(self):
        """Calculates the force and torque on a magnet due to all other magnets.

        This is a template that needs to be implemented for each magnet.
        """
        pass

    def _get_field_internal(self, x, y, z):
        """Internal magnetic field calculation method. This should be defined
        for each magnet type.

        Args:
            x (ndarray): x co-ordinates
            y (ndarray): y co-ordinates
            z (ndarray): z co-ordinates
        """
        pass

    def _generate_mask(self, x, y, z):
        """Generates mask of points inside a magnet

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates
        """
        pass

    def get_size(self):
        """Returns magnet dimesions

        Must be implemented for each magnet
        """
        pass

__init__(Jr, **kwargs)

Init Method

Parameters:

Name Type Description Default
Jr float

Signed remnant magnetisation

required
Kwargs

center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0) mask_magnet (bool): Flag to mask magnet or not in plots alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0 beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0 gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0

Source code in src/pymagnet/magnets/_magnet3D.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(self, Jr, **kwargs) -> None:
    """Init Method

    Args:
        Jr (float): Signed remnant magnetisation

    Kwargs:
        center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
        mask_magnet (bool): Flag to mask magnet or not in plots
        alpha (float): Magnet Orientation angle about z (degrees).
        Defaults to 0.0
        beta (float): Magnet Orientation angle about y (degrees).
        Defaults to 0.0
        gamma (float): Magnet Orientation angle about x (degrees).
        Defaults to 0.0
    """
    super().__init__()

    self.Jr = Jr

    self.center = kwargs.pop("center", _np.array([0.0, 0.0, 0.0]))
    self.center = _np.asarray(self.center)

    self._mask_magnet = kwargs.pop("mask_magnet", False)

    # if type(self.center) is tuple:
    #     center = Point3(self.center[0], self.center[1], self.center[2])
    # self.xc = self.center[0]
    # self.yc = self.center[1]
    # self.zc = self.center[2]

    self.alpha = kwargs.pop("alpha", 0.0)  # rotation angle about z
    self.beta = kwargs.pop("beta", 0.0)  # rotation angle about y
    self.gamma = kwargs.pop("gamma", 0.0)  # rotation angle about x

    self.alpha_rad = _np.deg2rad(self.alpha)
    self.beta_rad = _np.deg2rad(self.beta)
    self.gamma_rad = _np.deg2rad(self.gamma)

get_Jr()

Returns local magnetisation orientation

Must be implemented for all classes

Source code in src/pymagnet/magnets/_magnet3D.py
85
86
87
88
89
90
def get_Jr(self):
    """Returns local magnetisation orientation

    Must be implemented for all classes
    """
    pass

get_center()

Returns magnet center

Returns:

Type Description
ndarray

[center_x, center_y, center_z]

Source code in src/pymagnet/magnets/_magnet3D.py
77
78
79
80
81
82
83
def get_center(self):
    """Returns magnet center

    Returns:
        ndarray: [center_x, center_y, center_z]
    """
    return self.center

get_field(x, y, z)

Calculates the magnetic field at point(s) x,y,z due to a 3D magnet The calculations are always performed in local coordinates with the centre of the magnet at origin and z magnetisation pointing along the local z' axis.

The rotations and translations are performed first, and the internal field calculation functions are called.

Parameters:

Name Type Description Default
x ndarray

x co-ordinates

required
y ndarray

y co-ordinates

required
z ndarray

z co-ordinates

required

Returns:

Type Description
tuple

Bx(ndarray), By(ndarray), Bz(ndarray) field vector

Source code in src/pymagnet/magnets/_magnet3D.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def get_field(self, x, y, z):
    """Calculates the magnetic field at point(s) x,y,z due to a 3D magnet
    The calculations are always performed in local coordinates with the
    centre of the magnet at origin and z magnetisation pointing along the
    local z' axis.

    The rotations and translations are performed first, and the internal
    field calculation functions are called.

    Args:
        x (ndarray): x co-ordinates
        y (ndarray): y co-ordinates
        z (ndarray): z co-ordinates

    Returns:
        tuple: Bx(ndarray), By(ndarray), Bz(ndarray) field vector
    """
    from ..utils._routines3D import _apply_mask, _tile_arrays

    # If any rotation angle is set, transform the data
    if _np.any(
        _np.fabs(
            _np.array(
                [
                    self.alpha_rad,
                    self.beta_rad,
                    self.gamma_rad,
                ]
            )
        )
        > Magnet.tol
    ):
        forward_rotation, reverse_rotation = self._generate_rotation_quaternions()
        assert forward_rotation is not None
        assert reverse_rotation is not None
        # Generate 3xN array for quaternion rotation
        pos_vec = Quaternion._prepare_vector(
            x - self.center[0], y - self.center[1], z - self.center[2]
        )
        assert pos_vec is not None
        # Rotate points
        x_rot, y_rot, z_rot = forward_rotation * pos_vec  # type: ignore

        # Calls internal child method to calculate the field
        B_local = self._get_field_internal(x_rot, y_rot, z_rot)
        mask = self._generate_mask(x_rot, y_rot, z_rot)

        B_local = _apply_mask(self, B_local, mask)

        # Rearrange the field vectors in a 3xN array for quaternion rotation
        Bvec = Quaternion._prepare_vector(B_local.x, B_local.y, B_local.z)

        # Rotate the local fields back into the global frame using quaternions
        Bx, By, Bz = reverse_rotation * Bvec

        # finally return the fields
        return Bx, By, Bz

    else:
        # Otherwise directly calculate the magnetic fields
        B = self._get_field_internal(
            x - self.center[0], y - self.center[1], z - self.center[2]
        )

        xloc, yloc, zloc = _tile_arrays(
            x - self.center[0], y - self.center[1], z - self.center[2]
        )
        mask = self._generate_mask(xloc, yloc, zloc)
        B = _apply_mask(self, B, mask)

        return B.x, B.y, B.z

get_force_torque()

Calculates the force and torque on a magnet due to all other magnets.

This is a template that needs to be implemented for each magnet.

Source code in src/pymagnet/magnets/_magnet3D.py
209
210
211
212
213
214
def get_force_torque(self):
    """Calculates the force and torque on a magnet due to all other magnets.

    This is a template that needs to be implemented for each magnet.
    """
    pass

get_orientation()

Returns magnet orientation, alpha, beta, gamma in degrees

Returns:

Type Description
ndarray

alpha, beta, gamma rotation angles w.r.t z, y, and x axes

Source code in src/pymagnet/magnets/_magnet3D.py
92
93
94
95
96
97
98
99
def get_orientation(self):
    """Returns magnet orientation, `alpha`, `beta`, `gamma` in degrees

    Returns:
        ndarray: alpha, beta, gamma rotation angles w.r.t z, y, and x axes
    """

    return _np.array([self.alpha, self.beta, self.gamma])

get_size()

Returns magnet dimesions

Must be implemented for each magnet

Source code in src/pymagnet/magnets/_magnet3D.py
237
238
239
240
241
242
def get_size(self):
    """Returns magnet dimesions

    Must be implemented for each magnet
    """
    pass

Mesh

Bases: Magnet3D

Mesh Magnet Class.

Source code in src/pymagnet/magnets/_polygon3D.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
class Mesh(Magnet3D):
    """Mesh Magnet Class."""

    mag_type = "Mesh"

    def __init__(
        self,
        filename,
        Jr=1.0,  # local magnetisation
        **kwargs,
    ):
        """Init Method

        Args:
            filename (string): path to stl file to be imported
            Jr (float, optional): Signed remnant magnetisation. Defaults to 1.0.

        Kwargs:
            phi (float):
            theta (float):
            mesh_scale (float): scaling factor if mesh needs to be resized. Defaults to 1.0
        """
        super().__init__(Jr, **kwargs)

        self.phi = kwargs.pop("phi", 90.0)
        self.phi_rad = _np.deg2rad(self.phi)
        self.theta = kwargs.pop("theta", 0.0)
        self.theta_rad = _np.deg2rad(self.theta)

        self.mesh_scale = kwargs.pop("mesh_scale", 1.0)
        self._filename = filename

        (
            self.mesh_vectors,
            self.mesh_normals,
            self.volume,
            self.centroid,
        ) = self._import_mesh()

        self.Jx = _np.around(
            Jr * _np.cos(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
        )
        self.Jy = _np.around(
            Jr * _np.sin(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
        )
        self.Jz = _np.around(Jr * _np.cos(self.theta_rad), decimals=6)
        self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy
        self.J = _np.array([self.Jx, self.Jy, self.Jz])

        # FIXME: Sort out rotation of magnetisation with rotation of mesh
        # if _np.any(
        #     _np.fabs([self.alpha_rad, self.beta_rad, self.gamma_rad]) > ALIGN_CUTOFF
        # ):
        #     mag_rotation = Quaternion.gen_rotation_quaternion(
        #         self.alpha_rad, self.beta_rad, self.gamma_rad
        #     )
        #     Jrot = mag_rotation * self.J
        #     self.Jx = Jrot[0]
        #     self.Jy = Jrot[1]
        #     self.Jz = Jrot[2]
        #     self.J = _np.array([self.Jx, self.Jy, self.Jz])

        self.Jnorm = _np.dot(self.J, self.mesh_normals.T)

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def get_Jr(self):
        """Returns Magnetisation vector

        Returns:
            ndarray: [Jx, Jy, Jz]
        """
        return self.J

    def size(self):
        """Returns magnet dimesions

        Returns:
            size (ndarray): numpy array [width, depth, height]
        """
        pass

    def get_center(self):
        """Returns magnet center

        Returns:
            ndarray: magnet center
        """
        return self.center

    def get_field(self, x, y, z, parallel=True, r_cut=_np.inf):
        """Calculates the magnetic field at point(s) x,y,z due to a 3D magnet
        The calculations are always performed in local coordinates with the centre of the magnet at origin and z magnetisation pointing along the local z' axis.

        The rotations and translations are performed first, and the internal field calculation functions are called.

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates
            parallel (bool): If True, use parallel numba implementation
            r_cut (float): Distance cutoff. Triangles whose centroid is farther
                than ``r_cut`` from an evaluation point are skipped.  Units must
                match the mesh coordinates (typically mm).  Default: no cutoff.

        Returns:
            tuple: Bx(ndarray), By(ndarray), Bz(ndarray)  field vector
        """
        if parallel:
            B = self._get_field_parallel(x, y, z, r_cut=r_cut)
        else:
            B = self._get_field_internal(x, y, z)

        return B.x, B.y, B.z

    def _get_field_parallel(self, x, y, z, r_cut=_np.inf):
        """Parallel magnetic field calculation using numba.

        Delegates to :meth:`_get_field_parallel_pts`, which parallelises over
        evaluation points (prange outer loop) rather than triangles.  This is
        the only correct parallel strategy: the original triangles-outer kernel
        had a race condition — multiple threads writing to the same output
        array indices without synchronisation — producing non-deterministic
        errors up to ~10% on large meshes.

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates
            r_cut (float): Distance cutoff passed to
                :meth:`_get_field_parallel_pts`.  Default: no cutoff.

        Returns:
            Field3: Magnetic field array
        """
        return self._get_field_parallel_pts(x, y, z, r_cut=r_cut)

    def _get_field_serial_fast(self, x, y, z):
        """Serial but numba-optimized magnetic field calculation.

        Uses the numba-compiled functions but processes triangles serially.
        Useful for comparison with parallel version.

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates

        Returns:
            Field3: Magnetic field array
        """
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)
        vec_shape = B.x.shape

        x_flat = _np.asarray(x).ravel().astype(_np.float64)
        y_flat = _np.asarray(y).ravel().astype(_np.float64)
        z_flat = _np.asarray(z).ravel().astype(_np.float64)

        mesh_vectors = _np.ascontiguousarray(self.mesh_vectors, dtype=_np.float64)
        Jnorm = _np.ascontiguousarray(self.Jnorm, dtype=_np.float64)

        Bx, By, Bz = _get_field_serial_njit(
            mesh_vectors, Jnorm, self.Jr, x_flat, y_flat, z_flat
        )

        Bx[~_np.isfinite(Bx)] = 0.0
        By[~_np.isfinite(By)] = 0.0
        Bz[~_np.isfinite(Bz)] = 0.0

        B.x = Bx.reshape(vec_shape)
        B.y = By.reshape(vec_shape)
        B.z = Bz.reshape(vec_shape)
        B.n = _np.linalg.norm([B.x, B.y, B.z], axis=0)

        return B

    def _get_field_parallel_pts(self, x, y, z, r_cut=_np.inf):
        """Transposed parallel field calculation: prange over evaluation points.

        Precomputes per-triangle rotation data once, then parallelises over
        the evaluation-point axis rather than the triangle axis.  This layout
        enables per-point triangle culling via an optional distance cutoff.

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates
            r_cut (float): distance cutoff in the same units as the mesh
                coordinates.  Triangles whose centroid is farther than r_cut
                from an evaluation point are skipped.  Default: np.inf
                (no culling — full accuracy).

        Returns:
            Field3: Magnetic field array
        """
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)
        vec_shape = B.x.shape

        x_flat = _np.asarray(x).ravel().astype(_np.float64)
        y_flat = _np.asarray(y).ravel().astype(_np.float64)
        z_flat = _np.asarray(z).ravel().astype(_np.float64)

        mesh_vectors = _np.ascontiguousarray(self.mesh_vectors, dtype=_np.float64)
        Jnorm = _np.ascontiguousarray(self.Jnorm, dtype=_np.float64)

        rotations, offsets, RA_tris1, RA_tris2, swap_flags, active, centroids = (
            _precompute_triangle_data(mesh_vectors, Jnorm, self.Jr)
        )

        Bx, By, Bz = _get_field_parallel_pts_njit(
            rotations,
            offsets,
            RA_tris1,
            RA_tris2,
            swap_flags,
            active,
            centroids,
            Jnorm,
            x_flat,
            y_flat,
            z_flat,
            float(r_cut),
        )

        Bx[~_np.isfinite(Bx)] = 0.0
        By[~_np.isfinite(By)] = 0.0
        Bz[~_np.isfinite(Bz)] = 0.0

        B.x = Bx.reshape(vec_shape)
        B.y = By.reshape(vec_shape)
        B.z = Bz.reshape(vec_shape)
        B.n = _np.linalg.norm([B.x, B.y, B.z], axis=0)

        return B

    def get_force_torque(self, depth=4, unit="mm"):
        """Calculates the force and torque on a prism magnet due to all other magnets.

        Args:
            depth (int, optional): Number of recursions of division by 4 per simplex
            unit (str, optional): Length scale. Defaults to 'mm'.

        Returns:
            tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
        """
        from ..forces._mesh_force import calc_force_mesh

        force, torque = calc_force_mesh(self, depth, unit)
        return force, torque

    def _get_field_internal(self, x, y, z):
        """Internal magnetic field calculation methods.
        Iterates over each triangle that makes up the mesh magnet and calculates the magnetic field

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates

        Returns:
            Field3: Magnetic field array
        """
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)
        vec_shape = B.x.shape
        B.x = B.x.ravel()
        B.y = B.y.ravel()
        B.z = B.z.ravel()

        # debug for loop, used when needing to check certain triangles, or groups of triangles
        # for i in range(self.start, self.stop):
        for i in range(len(self.mesh_vectors)):
            if _np.fabs(self.Jnorm[i] / self.Jr) > 1e-4:
                Btx, Bty, Btz, _, _, _ = self.calcB_triangle(
                    self.mesh_vectors[i],
                    self.Jnorm[i],
                    x,
                    y,
                    z,
                    i,
                )

                B.x += Btx
                B.y += Bty
                B.z += Btz

        B.x = _np.reshape(B.x, vec_shape)
        B.y = _np.reshape(B.y, vec_shape)
        B.z = _np.reshape(B.z, vec_shape)

        B.n = _np.linalg.norm([B.x, B.y, B.z], axis=0)
        return B

    def _import_mesh(self):
        """Imports mesh from STL file

        Returns:
            tuple: mesh_vectors (ndarray of mesh triangles), mesh_normals (ndarray of normals to each triangle)
        """
        stl_mesh = mesh.Mesh.from_file(self._filename)

        if _np.any(
            _np.fabs([self.alpha_rad, self.beta_rad, self.gamma_rad]) > ALIGN_CUTOFF
        ):
            mesh_rotation = Quaternion.gen_rotation_quaternion(
                self.alpha_rad, self.beta_rad, self.gamma_rad
            )

            angle, axis = mesh_rotation.get_axisangle()
            stl_mesh.rotate(axis, angle)

        # to ensure that the initial center is set to the centroid
        _, centroid, _ = stl_mesh.get_mass_properties()
        stl_mesh.translate(-centroid)

        offset = self.get_center()
        stl_mesh.translate(offset / self.mesh_scale)

        # get values after translation
        volume, centroid, _ = stl_mesh.get_mass_properties()

        mesh_vectors = stl_mesh.vectors.astype(_np.float64)
        mesh_normals = stl_mesh.normals.astype(_np.float64)

        # scale values
        volume *= self.mesh_scale**3
        centroid *= self.mesh_scale
        mesh_vectors *= self.mesh_scale

        mesh_normals = mesh_normals / _np.linalg.norm(
            mesh_normals, axis=1, keepdims=True
        )

        return mesh_vectors, mesh_normals, volume, centroid

    def _generate_mask(self, x, y, z):
        """Generates mask of points inside a magnet
        NOTE: not implemented for Mesh magnets.
        Args:
            x (ndarray/float): x-coordinates
            y (ndarray/float): y-coordinates
            z (ndarray/float): z-coordinates
        """
        pass

    def calcB_triangle(self, triangle, Jr, x, y, z, i):
        """Calculates the magnetic field due to a triangle

        Args:
            triangle (ndarray): Vertices of a triangle
            Jr (float): Remnant magnetisation component normal to triangle
            x (ndarray): x coordinates
            y (ndarray): y coordinates
            z (ndarray): z coordinates

        Returns:
            tuple: Bx, By, Bz magnetic field components
        """

        (
            total_rotation,
            rotated_triangle,
            offset,
            RA_triangle1,
            RA_triangle2,
        ) = _rotate_triangle(triangle, Jr)

        # Prepare points and quaternion
        pos_vec = Quaternion._prepare_vector(x, y, z)

        # Rotate points
        x_rot, y_rot, z_rot = total_rotation * pos_vec

        norm1 = norm_plane(triangle)

        if _np.allclose(norm1, [0, -1, 0], atol=ALIGN_CUTOFF) and Jr < 0:
            RA_triangle1, RA_triangle2 = RA_triangle2, RA_triangle1

        Btx, Bty, Btz = self._calcB_2_triangles(
            RA_triangle1,
            RA_triangle2,
            Jr,
            x_rot - offset[0],
            y_rot - offset[1],
            z_rot - offset[2],
        )

        Bvec = Quaternion._prepare_vector(Btx, Bty, Btz)
        Bx, By, Bz = total_rotation.get_conjugate() * Bvec

        return Bx, By, Bz, rotated_triangle, offset, total_rotation

    def _calcB_2_triangles(self, triangle1, triangle2, Jr, x, y, z):
        """Calculates the magnetic field due to two split right angled triangles
        in their local frame.

        Args:
            triangle1 (ndarray): Vertices of triangle 1
            triangle2 (ndarray): Vertices of triangle 2
            Jr (float): normal remnant magnetisation
            x (ndarray): x coordinates
            y (ndarray): y coordinates
            z (ndarray): z coordinates

        Returns:
            tuple: Bx, By, Bz magnetic field components
        """

        # Calc RA1 Field
        Btx, Bty, Btz = self._charge_sheet(triangle1[0], triangle1[1], Jr, x, y, z)

        # Rotate into local of RA2
        rotate_about_z = q_angle_from_axis(PI, (0, 0, 1))
        pos_vec_RA2 = Quaternion._prepare_vector(x - triangle1[0], y, z)

        x_local, y_local, z_local = rotate_about_z * pos_vec_RA2

        # Calc RA2 Field
        Btx2, Bty2, Btz2 = self._charge_sheet(
            triangle2[0], triangle2[1], Jr, x_local + triangle2[0], y_local, z_local
        )

        # Inverse Rot of RA2 Field
        Bvec = Quaternion._prepare_vector(Btx2, Bty2, Btz2)
        Btx2, Bty2, Btz2 = rotate_about_z.get_conjugate() * Bvec

        Btx += Btx2
        Bty += Bty2
        Btz += Btz2

        return Btx, Bty, Btz

    @staticmethod
    def _charge_sheet(a, b, Jr, x, y, z):
        sigma = Jr
        with _np.errstate(all="ignore"):
            Bx = _charge_sheet_x(a, b, sigma, x, y, z)
            By = _charge_sheet_y(a, b, sigma, x, y, z)
            Bz = _charge_sheet_z(a, b, sigma, x, y, z)
        return Bx, By, Bz

__init__(filename, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
filename string

path to stl file to be imported

required
Jr float

Signed remnant magnetisation. Defaults to 1.0.

1.0
Kwargs

phi (float): theta (float): mesh_scale (float): scaling factor if mesh needs to be resized. Defaults to 1.0

Source code in src/pymagnet/magnets/_polygon3D.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(
    self,
    filename,
    Jr=1.0,  # local magnetisation
    **kwargs,
):
    """Init Method

    Args:
        filename (string): path to stl file to be imported
        Jr (float, optional): Signed remnant magnetisation. Defaults to 1.0.

    Kwargs:
        phi (float):
        theta (float):
        mesh_scale (float): scaling factor if mesh needs to be resized. Defaults to 1.0
    """
    super().__init__(Jr, **kwargs)

    self.phi = kwargs.pop("phi", 90.0)
    self.phi_rad = _np.deg2rad(self.phi)
    self.theta = kwargs.pop("theta", 0.0)
    self.theta_rad = _np.deg2rad(self.theta)

    self.mesh_scale = kwargs.pop("mesh_scale", 1.0)
    self._filename = filename

    (
        self.mesh_vectors,
        self.mesh_normals,
        self.volume,
        self.centroid,
    ) = self._import_mesh()

    self.Jx = _np.around(
        Jr * _np.cos(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
    )
    self.Jy = _np.around(
        Jr * _np.sin(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
    )
    self.Jz = _np.around(Jr * _np.cos(self.theta_rad), decimals=6)
    self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy
    self.J = _np.array([self.Jx, self.Jy, self.Jz])

    # FIXME: Sort out rotation of magnetisation with rotation of mesh
    # if _np.any(
    #     _np.fabs([self.alpha_rad, self.beta_rad, self.gamma_rad]) > ALIGN_CUTOFF
    # ):
    #     mag_rotation = Quaternion.gen_rotation_quaternion(
    #         self.alpha_rad, self.beta_rad, self.gamma_rad
    #     )
    #     Jrot = mag_rotation * self.J
    #     self.Jx = Jrot[0]
    #     self.Jy = Jrot[1]
    #     self.Jz = Jrot[2]
    #     self.J = _np.array([self.Jx, self.Jy, self.Jz])

    self.Jnorm = _np.dot(self.J, self.mesh_normals.T)

calcB_triangle(triangle, Jr, x, y, z, i)

Calculates the magnetic field due to a triangle

Parameters:

Name Type Description Default
triangle ndarray

Vertices of a triangle

required
Jr float

Remnant magnetisation component normal to triangle

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
tuple

Bx, By, Bz magnetic field components

Source code in src/pymagnet/magnets/_polygon3D.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def calcB_triangle(self, triangle, Jr, x, y, z, i):
    """Calculates the magnetic field due to a triangle

    Args:
        triangle (ndarray): Vertices of a triangle
        Jr (float): Remnant magnetisation component normal to triangle
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates

    Returns:
        tuple: Bx, By, Bz magnetic field components
    """

    (
        total_rotation,
        rotated_triangle,
        offset,
        RA_triangle1,
        RA_triangle2,
    ) = _rotate_triangle(triangle, Jr)

    # Prepare points and quaternion
    pos_vec = Quaternion._prepare_vector(x, y, z)

    # Rotate points
    x_rot, y_rot, z_rot = total_rotation * pos_vec

    norm1 = norm_plane(triangle)

    if _np.allclose(norm1, [0, -1, 0], atol=ALIGN_CUTOFF) and Jr < 0:
        RA_triangle1, RA_triangle2 = RA_triangle2, RA_triangle1

    Btx, Bty, Btz = self._calcB_2_triangles(
        RA_triangle1,
        RA_triangle2,
        Jr,
        x_rot - offset[0],
        y_rot - offset[1],
        z_rot - offset[2],
    )

    Bvec = Quaternion._prepare_vector(Btx, Bty, Btz)
    Bx, By, Bz = total_rotation.get_conjugate() * Bvec

    return Bx, By, Bz, rotated_triangle, offset, total_rotation

get_Jr()

Returns Magnetisation vector

Returns:

Type Description
ndarray

[Jx, Jy, Jz]

Source code in src/pymagnet/magnets/_polygon3D.py
107
108
109
110
111
112
113
def get_Jr(self):
    """Returns Magnetisation vector

    Returns:
        ndarray: [Jx, Jy, Jz]
    """
    return self.J

get_center()

Returns magnet center

Returns:

Type Description
ndarray

magnet center

Source code in src/pymagnet/magnets/_polygon3D.py
123
124
125
126
127
128
129
def get_center(self):
    """Returns magnet center

    Returns:
        ndarray: magnet center
    """
    return self.center

get_field(x, y, z, parallel=True, r_cut=_np.inf)

Calculates the magnetic field at point(s) x,y,z due to a 3D magnet The calculations are always performed in local coordinates with the centre of the magnet at origin and z magnetisation pointing along the local z' axis.

The rotations and translations are performed first, and the internal field calculation functions are called.

Parameters:

Name Type Description Default
x float / array

x co-ordinates

required
y float / array

y co-ordinates

required
z float / array

z co-ordinates

required
parallel bool

If True, use parallel numba implementation

True
r_cut float

Distance cutoff. Triangles whose centroid is farther than r_cut from an evaluation point are skipped. Units must match the mesh coordinates (typically mm). Default: no cutoff.

inf

Returns:

Type Description
tuple

Bx(ndarray), By(ndarray), Bz(ndarray) field vector

Source code in src/pymagnet/magnets/_polygon3D.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def get_field(self, x, y, z, parallel=True, r_cut=_np.inf):
    """Calculates the magnetic field at point(s) x,y,z due to a 3D magnet
    The calculations are always performed in local coordinates with the centre of the magnet at origin and z magnetisation pointing along the local z' axis.

    The rotations and translations are performed first, and the internal field calculation functions are called.

    Args:
        x (float/array): x co-ordinates
        y (float/array): y co-ordinates
        z (float/array): z co-ordinates
        parallel (bool): If True, use parallel numba implementation
        r_cut (float): Distance cutoff. Triangles whose centroid is farther
            than ``r_cut`` from an evaluation point are skipped.  Units must
            match the mesh coordinates (typically mm).  Default: no cutoff.

    Returns:
        tuple: Bx(ndarray), By(ndarray), Bz(ndarray)  field vector
    """
    if parallel:
        B = self._get_field_parallel(x, y, z, r_cut=r_cut)
    else:
        B = self._get_field_internal(x, y, z)

    return B.x, B.y, B.z

get_force_torque(depth=4, unit='mm')

Calculates the force and torque on a prism magnet due to all other magnets.

Parameters:

Name Type Description Default
depth int

Number of recursions of division by 4 per simplex

4
unit str

Length scale. Defaults to 'mm'.

'mm'

Returns:

Type Description
tuple

force (ndarray (3,) ) and torque (ndarray (3,) )

Source code in src/pymagnet/magnets/_polygon3D.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def get_force_torque(self, depth=4, unit="mm"):
    """Calculates the force and torque on a prism magnet due to all other magnets.

    Args:
        depth (int, optional): Number of recursions of division by 4 per simplex
        unit (str, optional): Length scale. Defaults to 'mm'.

    Returns:
        tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
    """
    from ..forces._mesh_force import calc_force_mesh

    force, torque = calc_force_mesh(self, depth, unit)
    return force, torque

size()

Returns magnet dimesions

Returns:

Type Description
size (ndarray

numpy array [width, depth, height]

Source code in src/pymagnet/magnets/_polygon3D.py
115
116
117
118
119
120
121
def size(self):
    """Returns magnet dimesions

    Returns:
        size (ndarray): numpy array [width, depth, height]
    """
    pass

PolyMagnet

Bases: Magnet2D

2D Magnet Polygon class.

Source code in src/pymagnet/magnets/_polygon2D.py
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
class PolyMagnet(Magnet2D):
    """2D Magnet Polygon class."""

    mag_type = "PolyMagnet"

    def __init__(self, Jr, **kwargs) -> None:
        """Init method

        NOTE:
            * When creating a regular polygon, one of apothem, radius, or length
              must be defined as a kwarg or an exception will be raised.
            * When creating a regular polygon, the number of sides `num_sides`
            must be at least 3 or an exception will be raised.
            * When creating a custom polygon at least one vertex pair must be
            defined with `vertices` or an exception will be raised.

        Args:
            Jr (float): signed magnitude of remnant magnetisation

        Kwargs:
            alpha (float): Not used
            theta (float): Orientation of magnet w.r.t x-axis of magnet
            phi (float): Orientation of magnetisation w.r.t x-axis of magnet in degrees.
            Defaults to 90.0.
            center (ndarray): magnet center (x, y). Defaults to (0.0, 0.0)
            length (float): side length if creating a regular polygon
            apothem (float): apothem (incircle radius) if creating a regular polygon
            radius (float): radius (circumcircle radius) if  creating a regular polygon
            num_sides (int): number of sides of a regular polygon. Defaults to 6.
            custom_polygon (bool): Flag to define a custom polygon. Defaults to False.
            vertices (ndarray, list): List of custom vertices. Defaults to None.

        Raises:
            Exception: If creating a custom polygon, `vertices` must not be None.
        """
        from ..utils._routines2D import rotate_points_2D

        super().__init__(Jr, **kwargs)

        # Magnet rotation w.r.t. x-axis
        self.alpha = kwargs.pop("alpha", 0.0)
        self.alpha_radians = _np.deg2rad(self.alpha)

        self.theta = kwargs.pop("theta", 0.0)
        self.theta_radians = _np.deg2rad(self.theta)

        self.phi = kwargs.pop("phi", 90.0)
        self.phi_rad = _np.deg2rad(self.phi)

        self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
        self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
        self.tol = MAG_TOL
        self.area = None

        self.custom_polygon = kwargs.pop("custom_polygon", False)

        self.center = kwargs.pop("center", _np.array([0.0, 0.0, 0.0]))
        self.center = _np.asarray(self.center)

        if self.custom_polygon:
            vertices = kwargs.pop("vertices", None)
            if vertices is None:
                raise ValueError("Error, no vertices were defined.")

            vertices = _np.atleast_2d(vertices)

            x_rot, y_rot = rotate_points_2D(
                vertices[:, 0],
                vertices[:, 1],
                self.theta_radians,  # + self.alpha_radians,
            )
            vertices = _np.stack([x_rot, y_rot]).T + self.center
            self.polygon = Polygon(vertices=vertices.tolist())
        else:
            self.length = kwargs.pop("length", None)
            self.apothem = kwargs.pop("apothem", None)
            self.radius = kwargs.pop("radius", None)
            self.num_sides = kwargs.pop("num_sides", 6)

            self.radius = Polygon.check_radius(
                self.num_sides,
                self.apothem,
                self.length,
                self.radius,
            )
            # Generate Polygon
            self.polygon = Polygon(
                vertices=Polygon.gen_polygon(
                    self.num_sides,
                    self.center,
                    self.theta,  # + self.alpha,
                    length=self.length,
                    apothem=self.apothem,
                    radius=self.radius,
                ),
                center=self.center,
            )

    def get_center(self):
        """Returns magnet centre

        Returns:
            center (ndarray): numpy array [xc, yc]
        """
        return self.center

    def get_orientation(self):
        """Returns magnet orientation, `alpha` in degrees

        Returns:
            float: alpha, rotation angle w.r.t x-axis.
        """

        return self.alpha

    def _gen_sheet_magnets(self):
        """Generates orientation, size, and centre of sheet magnets for a given
        polygon

        Returns:
            tuple: beta (ndarray), length (ndarray), centre (ndarray),
            K (ndarray) - sheet current density in tesla.
        """
        area, norms, beta, length, center = LineUtils.signed_area2D(self.polygon)
        K = self.Jx * norms[:, 1] - self.Jy * norms[:, 0]
        self.area = area
        return beta, length, center, K

    def get_field(self, x, y):
        """Calculates the magnetic field of a polygon due to each face

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            tuple: Bx (ndarray), By (ndarray) magnetic field vector
        """
        from ..utils._routines2D import _get_field_array_shape2

        array_shape = _get_field_array_shape2(x, y)
        Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)
        beta, length, center, K = self._gen_sheet_magnets()

        if _np.fabs(self.alpha_radians) > self.tol:
            pass
            print("Arbitrary rotation with alpha not yet implemented!!")

            # FIXME: rotate centres
            # xt, yt = rotate_points_2D(x - self.xc, y - self.yc, self.alpha_radians)
            # beta += self.alpha
            # xc_rot, yc_rot = rotate_points_2D(
            #     center[:, 0] - self.xc,
            #     center[:, 1] - self.yc,
            #     self.alpha_radians,
            # )
            # center[:, 0] = xc_rot
            # center[:, 1] = yc_rot
            #
            #
            # for i in range(len(K)):
            #     sheet = Line(length[i], center[i], beta[i], K[i])
            #     Btx, Bty = sheet.get_field(xt, yt)
            #     Btx, Bty = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)
            #     Bx += Btx
            #     By += Bty

        for i in range(len(K)):
            sheet = Line(length[i], center[i], beta[i], K[i])
            Btx, Bty = sheet.get_field(x, y)
            Bx += Btx
            By += Bty
        return Bx, By

__init__(Jr, **kwargs)

Init method

NOTE
  • When creating a regular polygon, one of apothem, radius, or length must be defined as a kwarg or an exception will be raised.
  • When creating a regular polygon, the number of sides num_sides must be at least 3 or an exception will be raised.
  • When creating a custom polygon at least one vertex pair must be defined with vertices or an exception will be raised.

Parameters:

Name Type Description Default
Jr float

signed magnitude of remnant magnetisation

required
Kwargs

alpha (float): Not used theta (float): Orientation of magnet w.r.t x-axis of magnet phi (float): Orientation of magnetisation w.r.t x-axis of magnet in degrees. Defaults to 90.0. center (ndarray): magnet center (x, y). Defaults to (0.0, 0.0) length (float): side length if creating a regular polygon apothem (float): apothem (incircle radius) if creating a regular polygon radius (float): radius (circumcircle radius) if creating a regular polygon num_sides (int): number of sides of a regular polygon. Defaults to 6. custom_polygon (bool): Flag to define a custom polygon. Defaults to False. vertices (ndarray, list): List of custom vertices. Defaults to None.

Raises:

Type Description
Exception

If creating a custom polygon, vertices must not be None.

Source code in src/pymagnet/magnets/_polygon2D.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def __init__(self, Jr, **kwargs) -> None:
    """Init method

    NOTE:
        * When creating a regular polygon, one of apothem, radius, or length
          must be defined as a kwarg or an exception will be raised.
        * When creating a regular polygon, the number of sides `num_sides`
        must be at least 3 or an exception will be raised.
        * When creating a custom polygon at least one vertex pair must be
        defined with `vertices` or an exception will be raised.

    Args:
        Jr (float): signed magnitude of remnant magnetisation

    Kwargs:
        alpha (float): Not used
        theta (float): Orientation of magnet w.r.t x-axis of magnet
        phi (float): Orientation of magnetisation w.r.t x-axis of magnet in degrees.
        Defaults to 90.0.
        center (ndarray): magnet center (x, y). Defaults to (0.0, 0.0)
        length (float): side length if creating a regular polygon
        apothem (float): apothem (incircle radius) if creating a regular polygon
        radius (float): radius (circumcircle radius) if  creating a regular polygon
        num_sides (int): number of sides of a regular polygon. Defaults to 6.
        custom_polygon (bool): Flag to define a custom polygon. Defaults to False.
        vertices (ndarray, list): List of custom vertices. Defaults to None.

    Raises:
        Exception: If creating a custom polygon, `vertices` must not be None.
    """
    from ..utils._routines2D import rotate_points_2D

    super().__init__(Jr, **kwargs)

    # Magnet rotation w.r.t. x-axis
    self.alpha = kwargs.pop("alpha", 0.0)
    self.alpha_radians = _np.deg2rad(self.alpha)

    self.theta = kwargs.pop("theta", 0.0)
    self.theta_radians = _np.deg2rad(self.theta)

    self.phi = kwargs.pop("phi", 90.0)
    self.phi_rad = _np.deg2rad(self.phi)

    self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
    self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
    self.tol = MAG_TOL
    self.area = None

    self.custom_polygon = kwargs.pop("custom_polygon", False)

    self.center = kwargs.pop("center", _np.array([0.0, 0.0, 0.0]))
    self.center = _np.asarray(self.center)

    if self.custom_polygon:
        vertices = kwargs.pop("vertices", None)
        if vertices is None:
            raise ValueError("Error, no vertices were defined.")

        vertices = _np.atleast_2d(vertices)

        x_rot, y_rot = rotate_points_2D(
            vertices[:, 0],
            vertices[:, 1],
            self.theta_radians,  # + self.alpha_radians,
        )
        vertices = _np.stack([x_rot, y_rot]).T + self.center
        self.polygon = Polygon(vertices=vertices.tolist())
    else:
        self.length = kwargs.pop("length", None)
        self.apothem = kwargs.pop("apothem", None)
        self.radius = kwargs.pop("radius", None)
        self.num_sides = kwargs.pop("num_sides", 6)

        self.radius = Polygon.check_radius(
            self.num_sides,
            self.apothem,
            self.length,
            self.radius,
        )
        # Generate Polygon
        self.polygon = Polygon(
            vertices=Polygon.gen_polygon(
                self.num_sides,
                self.center,
                self.theta,  # + self.alpha,
                length=self.length,
                apothem=self.apothem,
                radius=self.radius,
            ),
            center=self.center,
        )

get_center()

Returns magnet centre

Returns:

Type Description
center (ndarray

numpy array [xc, yc]

Source code in src/pymagnet/magnets/_polygon2D.py
432
433
434
435
436
437
438
def get_center(self):
    """Returns magnet centre

    Returns:
        center (ndarray): numpy array [xc, yc]
    """
    return self.center

get_field(x, y)

Calculates the magnetic field of a polygon due to each face

Parameters:

Name Type Description Default
x ndarray

x-coordinates

required
y ndarray

y-coordinates

required

Returns:

Type Description
tuple

Bx (ndarray), By (ndarray) magnetic field vector

Source code in src/pymagnet/magnets/_polygon2D.py
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
def get_field(self, x, y):
    """Calculates the magnetic field of a polygon due to each face

    Args:
        x (ndarray): x-coordinates
        y (ndarray): y-coordinates

    Returns:
        tuple: Bx (ndarray), By (ndarray) magnetic field vector
    """
    from ..utils._routines2D import _get_field_array_shape2

    array_shape = _get_field_array_shape2(x, y)
    Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)
    beta, length, center, K = self._gen_sheet_magnets()

    if _np.fabs(self.alpha_radians) > self.tol:
        pass
        print("Arbitrary rotation with alpha not yet implemented!!")

        # FIXME: rotate centres
        # xt, yt = rotate_points_2D(x - self.xc, y - self.yc, self.alpha_radians)
        # beta += self.alpha
        # xc_rot, yc_rot = rotate_points_2D(
        #     center[:, 0] - self.xc,
        #     center[:, 1] - self.yc,
        #     self.alpha_radians,
        # )
        # center[:, 0] = xc_rot
        # center[:, 1] = yc_rot
        #
        #
        # for i in range(len(K)):
        #     sheet = Line(length[i], center[i], beta[i], K[i])
        #     Btx, Bty = sheet.get_field(xt, yt)
        #     Btx, Bty = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)
        #     Bx += Btx
        #     By += Bty

    for i in range(len(K)):
        sheet = Line(length[i], center[i], beta[i], K[i])
        Btx, Bty = sheet.get_field(x, y)
        Bx += Btx
        By += Bty
    return Bx, By

get_orientation()

Returns magnet orientation, alpha in degrees

Returns:

Type Description
float

alpha, rotation angle w.r.t x-axis.

Source code in src/pymagnet/magnets/_polygon2D.py
440
441
442
443
444
445
446
447
def get_orientation(self):
    """Returns magnet orientation, `alpha` in degrees

    Returns:
        float: alpha, rotation angle w.r.t x-axis.
    """

    return self.alpha

Polygon

Polygon class for generating list of vertices

Source code in src/pymagnet/magnets/_polygon2D.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
class Polygon:
    """Polygon class for generating list of vertices"""

    def __init__(self, **kwargs):
        vertices = kwargs.pop("vertices", None)
        center = kwargs.pop("center", None)
        if vertices is not None:
            if type(vertices) is _np.ndarray:
                if center is not None:
                    vertices += center
                self.vertices = vertices.tolist()
            else:
                self.vertices = vertices

            if center is not None:
                self.center = center
            else:
                self.set_center()
        else:
            self.vertices = []
            self.center = _np.nan

    def append(self, vertex):
        """Appends vertex to list of vertices

        Args:
            vertex (list): list of vertices
        """
        if len(vertex) != 2:
            print("Error")
        if type(vertex) is tuple:
            self.vertices.append(vertex)
        elif len(vertex) == 2:
            self.vertices.append(tuple(vertex))
        self.set_center()

    def num_vertices(self):
        """Gets number of vertices

        Returns:
            int: number of vertices
        """
        return len(self.vertices)

    def set_center(self):
        """Sets center of polygon to be centroid"""
        # FIXME: This is not the correct method!!! It should be the weighted mean
        self.center = _np.mean(_np.asarray(self.vertices), axis=0)

    @staticmethod
    def get_centroid_area(vertex_array):
        sumCx = 0
        sumCy = 0
        sumAc = 0
        for i in range(len(vertex_array) - 1):
            cX = (vertex_array[i][0] + vertex_array[i + 1][0]) * (
                vertex_array[i][0] * vertex_array[i + 1][1]
                - vertex_array[i + 1][0] * vertex_array[i][1]
            )
            cY = (vertex_array[i][1] + vertex_array[i + 1][1]) * (
                vertex_array[i][0] * vertex_array[i + 1][1]
                - vertex_array[i + 1][0] * vertex_array[i][1]
            )
            pA = (vertex_array[i][0] * vertex_array[i + 1][1]) - (
                vertex_array[i + 1][0] * vertex_array[i][1]
            )
            sumCx += cX
            sumCy += cY
            sumAc += pA
        area = sumAc / 2.0
        center = ((1.0 / (6.0 * area)) * sumCx, (1.0 / (6.0 * area)) * sumCy)
        return center, area

    @staticmethod
    def gen_polygon(N=6, center=(0.0, 0.0), alpha=0.0, **kwargs):
        """Generates regular polygon. One of apothem, side length or radius must
        be defined.

        Args:
            N (int, optional): Number of sides. Defaults to 6.
            center (tuple, optional): Polygon center. Defaults to (0.0, 0.0).
            alpha (float, optional): Orientration with respect to x-axis.
                Defaults to 0.0.

        Raises:
            Exception: N must be > 2

        Returns:
            ndarray: polygon vertices
        """
        N = int(N)

        if N < 3:
            raise ValueError("Error, N must be > 2.")

        apothem = kwargs.pop("apothem", None)
        length = kwargs.pop("length", None)
        radius = kwargs.pop("radius", None)

        radius = Polygon.check_radius(N, apothem, length, radius)

        k = _np.arange(0, N, 1)
        xc = center[0]
        yc = center[1]

        def f(N):
            if N % 2 == 0:
                return PI / N + _np.deg2rad(alpha)
            else:
                return PI / N + PI + _np.deg2rad(alpha)

        xv = xc + radius * _np.sin(2 * PI * k / N + f(N))
        yv = yc + radius * _np.cos(2 * PI * k / N + f(N))
        poly_verts = _np.vstack((xv, yv)).T.tolist()

        return poly_verts

    @staticmethod
    def check_radius(N, apothem, length, radius):
        """Checks which of apothem, side length, or radius has been passed as kwargs
        to `gen_polygon()`. Order of precendence is apothem, length, radius.

        Args:
            N (int): Number of sides
            apothem (float): polygon apothem
            length (float): side length
            radius (float): outcircle radius

        Raises:
            Exception: One of apothem, length, or raduis must be defined

        Returns:
            float: returns radius
        """
        if apothem is not None:
            return apothem / _np.around(_np.cos(PI / N), 4)
        elif length is not None:
            return length / _np.around(2 * _np.sin(PI / N), 4)
        elif radius is not None:
            return radius
        else:
            raise ValueError(
                "Error, one of apothem, length, or radius must be defined."
            )

append(vertex)

Appends vertex to list of vertices

Parameters:

Name Type Description Default
vertex list

list of vertices

required
Source code in src/pymagnet/magnets/_polygon2D.py
51
52
53
54
55
56
57
58
59
60
61
62
63
def append(self, vertex):
    """Appends vertex to list of vertices

    Args:
        vertex (list): list of vertices
    """
    if len(vertex) != 2:
        print("Error")
    if type(vertex) is tuple:
        self.vertices.append(vertex)
    elif len(vertex) == 2:
        self.vertices.append(tuple(vertex))
    self.set_center()

check_radius(N, apothem, length, radius) staticmethod

Checks which of apothem, side length, or radius has been passed as kwargs to gen_polygon(). Order of precendence is apothem, length, radius.

Parameters:

Name Type Description Default
N int

Number of sides

required
apothem float

polygon apothem

required
length float

side length

required
radius float

outcircle radius

required

Raises:

Type Description
Exception

One of apothem, length, or raduis must be defined

Returns:

Type Description
float

returns radius

Source code in src/pymagnet/magnets/_polygon2D.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
@staticmethod
def check_radius(N, apothem, length, radius):
    """Checks which of apothem, side length, or radius has been passed as kwargs
    to `gen_polygon()`. Order of precendence is apothem, length, radius.

    Args:
        N (int): Number of sides
        apothem (float): polygon apothem
        length (float): side length
        radius (float): outcircle radius

    Raises:
        Exception: One of apothem, length, or raduis must be defined

    Returns:
        float: returns radius
    """
    if apothem is not None:
        return apothem / _np.around(_np.cos(PI / N), 4)
    elif length is not None:
        return length / _np.around(2 * _np.sin(PI / N), 4)
    elif radius is not None:
        return radius
    else:
        raise ValueError(
            "Error, one of apothem, length, or radius must be defined."
        )

gen_polygon(N=6, center=(0.0, 0.0), alpha=0.0, **kwargs) staticmethod

Generates regular polygon. One of apothem, side length or radius must be defined.

Parameters:

Name Type Description Default
N int

Number of sides. Defaults to 6.

6
center tuple

Polygon center. Defaults to (0.0, 0.0).

(0.0, 0.0)
alpha float

Orientration with respect to x-axis. Defaults to 0.0.

0.0

Raises:

Type Description
Exception

N must be > 2

Returns:

Type Description
ndarray

polygon vertices

Source code in src/pymagnet/magnets/_polygon2D.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
@staticmethod
def gen_polygon(N=6, center=(0.0, 0.0), alpha=0.0, **kwargs):
    """Generates regular polygon. One of apothem, side length or radius must
    be defined.

    Args:
        N (int, optional): Number of sides. Defaults to 6.
        center (tuple, optional): Polygon center. Defaults to (0.0, 0.0).
        alpha (float, optional): Orientration with respect to x-axis.
            Defaults to 0.0.

    Raises:
        Exception: N must be > 2

    Returns:
        ndarray: polygon vertices
    """
    N = int(N)

    if N < 3:
        raise ValueError("Error, N must be > 2.")

    apothem = kwargs.pop("apothem", None)
    length = kwargs.pop("length", None)
    radius = kwargs.pop("radius", None)

    radius = Polygon.check_radius(N, apothem, length, radius)

    k = _np.arange(0, N, 1)
    xc = center[0]
    yc = center[1]

    def f(N):
        if N % 2 == 0:
            return PI / N + _np.deg2rad(alpha)
        else:
            return PI / N + PI + _np.deg2rad(alpha)

    xv = xc + radius * _np.sin(2 * PI * k / N + f(N))
    yv = yc + radius * _np.cos(2 * PI * k / N + f(N))
    poly_verts = _np.vstack((xv, yv)).T.tolist()

    return poly_verts

num_vertices()

Gets number of vertices

Returns:

Type Description
int

number of vertices

Source code in src/pymagnet/magnets/_polygon2D.py
65
66
67
68
69
70
71
def num_vertices(self):
    """Gets number of vertices

    Returns:
        int: number of vertices
    """
    return len(self.vertices)

set_center()

Sets center of polygon to be centroid

Source code in src/pymagnet/magnets/_polygon2D.py
73
74
75
76
def set_center(self):
    """Sets center of polygon to be centroid"""
    # FIXME: This is not the correct method!!! It should be the weighted mean
    self.center = _np.mean(_np.asarray(self.vertices), axis=0)

Prism

Bases: Magnet3D

Prism 3D Magnet Class

Returns:

Type Description
Prism

Prism magnet object

Source code in src/pymagnet/magnets/_magnet3D.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
class Prism(Magnet3D):
    """Prism 3D Magnet Class

    Returns:
        Prism: Prism magnet object
    """

    mag_type = "Prism"

    def __init__(
        self,
        width=10.0,
        depth=20.0,
        height=30.0,  # magnet dimensions
        Jr=1.0,  # local magnetisation direction
        **kwargs,
    ):
        """Init Method

        Args:
            width (float, optional): Magnet width in x. Defaults to 10.0.
            depth (float, optional): Magnet depth in y. Defaults to 20.0.
            height (float, optional): Magnet height in z. Defaults to 30.0.

        Kwargs:
            center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
            mask_magnet (bool): Flag to mask magnet or not in plots
            alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0
            beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0
            gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0
            phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0
            theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0
        """
        self.width = width
        self.depth = depth
        self.height = height

        self.a = width / 2
        self.b = depth / 2
        self.c = height / 2

        super().__init__(Jr, **kwargs)

        self.phi = kwargs.pop("phi", 90.0)
        self.phi_rad = _np.deg2rad(self.phi)
        self.theta = kwargs.pop("theta", 0.0)
        self.theta_rad = _np.deg2rad(self.theta)

        # Generate components of magnetisation
        self.Jx = _np.around(
            Jr * _np.cos(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
        )
        self.Jy = _np.around(
            Jr * _np.sin(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
        )
        self.Jz = _np.around(Jr * _np.cos(self.theta_rad), decimals=6)
        self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()} \n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()} \n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def get_Jr(self):
        """Returns magnetisation vector J

        Returns:
            ndarray: [Jx, Jy, Jz]
        """
        return _np.array([self.Jx, self.Jy, self.Jz])

    def get_size(self):
        """Returns magnet dimesions

        Returns:
        ndarray: [width, depth, height]
        """
        return _np.array([self.width, self.depth, self.height])

    def get_force_torque(self, num_samples=20, unit="mm"):
        """Calculates the force and torque on a prism magnet due to all other magnets.

        Args:
            num_samples (int, optional): Number of samples per axis per face. Defaults to 20.
            unit (str, optional): Length scale. Defaults to 'mm'.

        Returns:
            tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
        """
        from ..forces._prism_force import calc_force_prism

        force, torque = calc_force_prism(self, num_samples, unit)
        return force, torque

    @staticmethod
    def _F1(a, b, c, x, y, z):
        """Helper Function F1 for 3D prismatic magnets

        Args:
            a (float): magnet half width
            b (float): magnet half depth
            c (float): magnet half height
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates

        Returns:
            ndarray: F1 values
        """
        try:
            # Hide the warning for situtations where there is a divide by zero.
            # This returns a NaN in the array, which is ignored for plotting.
            with _np.errstate(divide="ignore", invalid="ignore"):
                data = _np.arctan(
                    ((y + b) * (z + c))
                    / (
                        (x + a)
                        * _np.sqrt(
                            _np.power((x + a), 2)
                            + _np.power((y + b), 2)
                            + _np.power((z + c), 2)
                        )
                    )
                )
        except ValueError:
            data = _np.nan
        return data

    @staticmethod
    def _F2(a, b, c, x, y, z):
        """Helper Function F2 for 3D prismatic magnets

        Args:
            a (float): magnet half width
            b (float): magnet half depth
            c (float): magnet half height
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates

        Returns:
            ndarray: F2 values
        """
        try:
            xa_sq = _np.power((x + a), 2)
            yb_sq = _np.power((y + b), 2)
            zc_sq = _np.power((z + c), 2)
            znc_sq = _np.power((z - c), 2)
            # Hide the warning for situtations where there is a divide by zero.
            # This returns a NaN in the array, which is ignored for plotting.
            with _np.errstate(divide="ignore", invalid="ignore"):
                data = (_np.sqrt(xa_sq + yb_sq + znc_sq) + c - z) / (
                    _np.sqrt(xa_sq + yb_sq + zc_sq) - c - z
                )
        except ValueError:
            data = _np.nan
        return data

    def _get_field_internal(self, x, y, z):
        """Internal magnetic field calculation methods.
        Iterates over each component of the prism magnetised in x, y, and z
        (in local coordinates).

        Args:
            x (ndarray): x co-ordinates
            y (ndarray): y co-ordinates
            z (ndarray): z co-ordinates

        Returns:
            Field3: Magnetic field array structure
        """
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)
        # Magnetic field due to component of M magnetised in x
        if _np.fabs(self.Jx) > self.tol:
            Bx, By, Bz = self._calcB_prism_x(x, y, z)
            B.x += Bx
            B.y += By
            B.z += Bz

        # Magnetic field due to component of M magnetised in y
        if _np.fabs(self.Jy) > self.tol:
            Bx, By, Bz = self._calcB_prism_y(x, y, z)
            B.x += Bx
            B.y += By
            B.z += Bz

        # Magnetic field due to component of M magnetised in z
        if _np.fabs(self.Jz) > self.tol:
            Bx, By, Bz = self._calcB_prism_z(x, y, z)
            B.x += Bx
            B.y += By
            B.z += Bz
        return B

    def _calcBx_prism_x(self, a, b, c, Jr, x, y, z):
        """Calculates x component of magnetic field for prism magnet
        magnetised in x

        Args:
            Args:
            a (float): magnet half width
            b (float): magnet half depth
            c (float): magnet half height
            Jr (float): Remnant magnetisation
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates

        Returns:
            ndarray: Bx magnetic field component
        """

        try:
            data = -(Jr / (4 * PI)) * (
                self._F1(a, b, c, -x, y, z)
                + self._F1(a, b, c, -x, y, -z)
                + self._F1(a, b, c, -x, -y, z)
                + self._F1(a, b, c, -x, -y, -z)
                + self._F1(a, b, c, x, y, z)
                + self._F1(a, b, c, x, y, -z)
                + self._F1(a, b, c, x, -y, z)
                + self._F1(a, b, c, x, -y, -z)
            )
        except ValueError:
            data = _np.nan
        return data

    def _calcBy_prism_x(self, a, b, c, Jr, x, y, z):
        """Calculates y component of magnetic field for prism magnet
        magnetised in x

        Args:
            a (float): magnet half width
            b (float): magnet half depth
            c (float): magnet half height
            Jr (float): Remnant magnetisation
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates

        Returns:
            ndarray: By magnetic field component
        """

        try:
            data = _np.log(
                self._F2(a, b, c, -x, -y, z)
                * self._F2(a, b, c, x, y, z)
                / (self._F2(a, b, c, -x, y, z) * self._F2(a, b, c, x, -y, z))
            )
            data *= Jr / (4 * PI)
        except ValueError:
            data = _np.nan
        return data

    def _calcBz_prism_x(self, a, b, c, Jr, x, y, z):
        """Calculates z component of magnetic field for prism magnet
        magnetised in x

        Args:
            a (float): magnet half width
            b (float): magnet half depth
            c (float): magnet half height
            Jr (float): Remnant magnetisation
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates

        Returns:
            ndarray: Bz magnetic field component
        """
        try:
            # Hide the warning for situtations where there is a divide by zero.
            # This returns a NaN in the array, which is ignored for plotting.
            with _np.errstate(divide="ignore", invalid="ignore"):
                data = _np.log(
                    self._F2(a, c, b, -x, -z, y)
                    * self._F2(a, c, b, x, z, y)
                    / (self._F2(a, c, b, -x, z, y) * self._F2(a, c, b, x, -z, y))
                )
            data *= Jr / (4 * PI)
        except ValueError:
            data = _np.nan
        return data

    def _calcB_prism_x(self, x, y, z):
        """Calculates magnetic field vector due to magnet magnetised in x

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates

        Returns:
            tuple: Bx (ndarray), By (ndarray), Bz (ndarray)
        """

        a = self.a
        b = self.b
        c = self.c
        Jr = self.Jx

        Bx = self._calcBx_prism_x(a, b, c, Jr, x, y, z)
        By = self._calcBy_prism_x(a, b, c, Jr, x, y, z)
        Bz = self._calcBz_prism_x(a, b, c, Jr, x, y, z)
        return Bx, By, Bz

    def _calcB_prism_z(self, x, y, z):
        """Calculates agnetic field vector due to magnet magnetised in y

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates

        Returns:
            tuple: Bx (ndarray), By (ndarray), Bz (ndarray)
        """
        a = self.a
        b = self.b
        c = self.c
        Jr = self.Jz

        Bx = self._calcBz_prism_x(-c, b, a, Jr, -z, y, x)
        By = self._calcBy_prism_x(-c, b, a, Jr, -z, y, x)
        Bz = -1 * self._calcBx_prism_x(-c, b, a, Jr, -z, y, x)
        return Bx, By, Bz

    def _calcB_prism_y(self, x, y, z):
        """Calculates agnetic field vector due to magnet magnetised in z

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates

        Returns:
            tuple: Bx (ndarray), By (ndarray), Bz (ndarray)
        """
        a = self.a
        b = self.b
        c = self.c
        Jr = self.Jy

        Bx = self._calcBy_prism_x(-b, a, c, Jr, -y, x, z)
        By = -1 * self._calcBx_prism_x(-b, a, c, Jr, -y, x, z)
        Bz = self._calcBz_prism_x(-b, a, c, Jr, -y, x, z)
        return Bx, By, Bz

    def _generate_mask(self, x, y, z):
        """Generates mask of points inside a magnet

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates
            z (ndarray): z-coordinates
        """
        w, d, h = self.get_size()
        xn = -w / 2
        xp = w / 2
        yn = -d / 2
        yp = d / 2
        zn = -h / 2
        zp = h / 2

        mask_x = _np.logical_and(x > xn, x < xp)
        mask_y = _np.logical_and(y > yn, y < yp)
        mask_z = _np.logical_and(z > zn, z < zp)

        # merge logical masks
        mask = _np.logical_and(mask_x, mask_z)
        mask = _np.logical_and(mask, mask_y)

        return mask

__init__(width=10.0, depth=20.0, height=30.0, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
width float

Magnet width in x. Defaults to 10.0.

10.0
depth float

Magnet depth in y. Defaults to 20.0.

20.0
height float

Magnet height in z. Defaults to 30.0.

30.0
Kwargs

center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0) mask_magnet (bool): Flag to mask magnet or not in plots alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0 beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0 gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0 phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0 theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0

Source code in src/pymagnet/magnets/_magnet3D.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
def __init__(
    self,
    width=10.0,
    depth=20.0,
    height=30.0,  # magnet dimensions
    Jr=1.0,  # local magnetisation direction
    **kwargs,
):
    """Init Method

    Args:
        width (float, optional): Magnet width in x. Defaults to 10.0.
        depth (float, optional): Magnet depth in y. Defaults to 20.0.
        height (float, optional): Magnet height in z. Defaults to 30.0.

    Kwargs:
        center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
        mask_magnet (bool): Flag to mask magnet or not in plots
        alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0
        beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0
        gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0
        phi (float): Angle of magnetisation vector (in degrees) with respect to x-axis. Defaults to 90.0
        theta (float): Angle of magnetisation vector (in degrees) with respect to z-axis. Defaults to 0.0
    """
    self.width = width
    self.depth = depth
    self.height = height

    self.a = width / 2
    self.b = depth / 2
    self.c = height / 2

    super().__init__(Jr, **kwargs)

    self.phi = kwargs.pop("phi", 90.0)
    self.phi_rad = _np.deg2rad(self.phi)
    self.theta = kwargs.pop("theta", 0.0)
    self.theta_rad = _np.deg2rad(self.theta)

    # Generate components of magnetisation
    self.Jx = _np.around(
        Jr * _np.cos(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
    )
    self.Jy = _np.around(
        Jr * _np.sin(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
    )
    self.Jz = _np.around(Jr * _np.cos(self.theta_rad), decimals=6)
    self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

get_Jr()

Returns magnetisation vector J

Returns:

Type Description
ndarray

[Jx, Jy, Jz]

Source code in src/pymagnet/magnets/_magnet3D.py
323
324
325
326
327
328
329
def get_Jr(self):
    """Returns magnetisation vector J

    Returns:
        ndarray: [Jx, Jy, Jz]
    """
    return _np.array([self.Jx, self.Jy, self.Jz])

get_force_torque(num_samples=20, unit='mm')

Calculates the force and torque on a prism magnet due to all other magnets.

Parameters:

Name Type Description Default
num_samples int

Number of samples per axis per face. Defaults to 20.

20
unit str

Length scale. Defaults to 'mm'.

'mm'

Returns:

Type Description
tuple

force (ndarray (3,) ) and torque (ndarray (3,) )

Source code in src/pymagnet/magnets/_magnet3D.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
def get_force_torque(self, num_samples=20, unit="mm"):
    """Calculates the force and torque on a prism magnet due to all other magnets.

    Args:
        num_samples (int, optional): Number of samples per axis per face. Defaults to 20.
        unit (str, optional): Length scale. Defaults to 'mm'.

    Returns:
        tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
    """
    from ..forces._prism_force import calc_force_prism

    force, torque = calc_force_prism(self, num_samples, unit)
    return force, torque

get_size()

Returns magnet dimesions

Returns: ndarray: [width, depth, height]

Source code in src/pymagnet/magnets/_magnet3D.py
331
332
333
334
335
336
337
def get_size(self):
    """Returns magnet dimesions

    Returns:
    ndarray: [width, depth, height]
    """
    return _np.array([self.width, self.depth, self.height])

Rectangle

Bases: Magnet2D

Rectangular 2D Magnet Class

Source code in src/pymagnet/magnets/_magnet2D.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
class Rectangle(Magnet2D):
    """Rectangular 2D Magnet Class"""

    mag_type = "Rectangle"

    def __init__(self, width=20.0, height=40.0, Jr=1.0, **kwargs):
        """Init Method

        Args:
            width (float, optional): Magnet Width. Defaults to 20.0.
            height (float, optional): Magnet Height. Defaults to 40.0.
            Jr (float, optional): Remnant Magnetisation. Defaults to 1.0.

        Kwargs:
            alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
            center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
            phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
        """
        super().__init__(Jr, **kwargs)
        self.width = width
        self.height = height

        self.a = width / 2
        self.b = height / 2

        self.phi = kwargs.pop("phi", 90)
        self.phi_rad = _np.deg2rad(self.phi)

        self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
        self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
        self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

    def get_size(self):
        """Returns magnet dimesions

        Returns:
            ndarray: numpy array [width, height]
        """
        return _np.array([self.width, self.height])

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation: alpha {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation: alpha {self.get_orientation()}\n"
        )
        return str

    def get_Jr(self):
        """Returns Magnetisation vector

        Returns:
            ndarray: [Jx, Jy]
        """
        return _np.array([self.Jx, self.Jy])

    def get_field(self, x, y):
        """Calculates the magnetic field at point(s) x,y due to a rectangular magnet

        Args:
            x (ndarray): x co-ordinates
            y (ndarray): y co-ordinates

        Returns:
            tuple: magnetic field vector Bx (ndarray), By (ndarray)
        """
        from ..utils._routines2D import _get_field_array_shape2, rotate_points_2D

        array_shape = _get_field_array_shape2(x, y)
        Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)

        if _np.fabs(self.alpha_radians) > Magnet2D.tol:
            xi, yi = rotate_points_2D(
                x - self.center[0], y - self.center[1], self.alpha_radians
            )

        # Calculate field due to x-component of magnetisation
        if _np.fabs(self.Jx / self.Jr) > Magnet2D.tol:
            if _np.fabs(self.alpha_radians) > Magnet2D.tol:
                # Calculate fields in local frame
                Btx = self._calcBx_mag_x(xi, yi)
                Bty = self._calcBy_mag_x(xi, yi)

                # Rotate fields to global frame
                Bx, By = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)

            else:
                Bx = self._calcBx_mag_x(x - self.center[0], y - self.center[1])
                By = self._calcBy_mag_x(x - self.center[0], y - self.center[1])

        # Calculate field due to y-component of magnetisation
        if _np.fabs(self.Jy / self.Jr) > Magnet2D.tol:
            if _np.fabs(self.alpha_radians) > Magnet2D.tol:
                Btx = self._calcBx_mag_y(xi, yi)
                Bty = self._calcBy_mag_y(xi, yi)

                Bxt, Byt = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)
                Bx += Bxt
                By += Byt
            else:
                Bx += self._calcBx_mag_y(x - self.center[0], y - self.center[1])
                By += self._calcBy_mag_y(x - self.center[0], y - self.center[1])
        return Bx, By

    def _calcBx_mag_x(self, x, y):
        """Bx using 2D Model for rectangular sheets magnetised in x-plane

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            ndarray: Bx, x component of magnetic field
        """
        a = self.a
        b = self.b
        J = self.Jx
        # Hide the warning for situtations where there is a divide by zero.
        # This returns a NaN in the array, which is ignored for plotting.
        with _np.errstate(divide="ignore", invalid="ignore"):
            return (J / (2 * PI)) * (
                _np.arctan2((2 * a * (b + y)), (x**2 - a**2 + (y + b) ** 2))
                + _np.arctan2((2 * a * (b - y)), (x**2 - a**2 + (y - b) ** 2))
            )

    def _calcBy_mag_x(self, x, y):
        """By using 2D Model for rectangular sheets magnetised in x-plane

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            ndarray: By, x component of magnetic field
        """
        a = self.a
        b = self.b
        J = self.Jx
        # Hide the warning for situtations where there is a divide by zero.
        # This returns a NaN in the array, which is ignored for plotting.
        with _np.errstate(divide="ignore", invalid="ignore"):
            return (-J / (4 * PI)) * (
                _np.log(((x - a) ** 2 + (y - b) ** 2) / ((x + a) ** 2 + (y - b) ** 2))
                - _np.log(((x - a) ** 2 + (y + b) ** 2) / ((x + a) ** 2 + (y + b) ** 2))
            )

    def _calcBx_mag_y(self, x, y):
        """Bx using 2D Model for rectangular sheets magnetised in y-plane

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            ndarray: Bx, x component of magnetic field
        """
        a = self.a
        b = self.b
        J = self.Jy
        # Hide the warning for situtations where there is a divide by zero.
        # This returns a NaN in the array, which is ignored for plotting.
        with _np.errstate(divide="ignore", invalid="ignore"):
            return (J / (4 * PI)) * (
                _np.log(((x + a) ** 2 + (y - b) ** 2) / ((x + a) ** 2 + (y + b) ** 2))
                - _np.log(((x - a) ** 2 + (y - b) ** 2) / ((x - a) ** 2 + (y + b) ** 2))
            )

    def _calcBy_mag_y(self, x: float, y: float) -> float:
        """Bx using 2D Model for rectangular sheets magnetised in y-plane

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            ndarray: By, x component of magnetic field
        """
        a = self.a
        b = self.b
        J = self.Jy
        return (J / (2 * PI)) * (
            _np.arctan2((2 * b * (x + a)), ((x + a) ** 2 + y**2 - b**2))
            - _np.arctan2((2 * b * (x - a)), ((x - a) ** 2 + y**2 - b**2))
        )

__init__(width=20.0, height=40.0, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
width float

Magnet Width. Defaults to 20.0.

20.0
height float

Magnet Height. Defaults to 40.0.

40.0
Jr float

Remnant Magnetisation. Defaults to 1.0.

1.0
Kwargs

alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0. center (tuple or ndarray): magnet center (x, y). Defaults to (0,0). phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.

Source code in src/pymagnet/magnets/_magnet2D.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def __init__(self, width=20.0, height=40.0, Jr=1.0, **kwargs):
    """Init Method

    Args:
        width (float, optional): Magnet Width. Defaults to 20.0.
        height (float, optional): Magnet Height. Defaults to 40.0.
        Jr (float, optional): Remnant Magnetisation. Defaults to 1.0.

    Kwargs:
        alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
        center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
        phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
    """
    super().__init__(Jr, **kwargs)
    self.width = width
    self.height = height

    self.a = width / 2
    self.b = height / 2

    self.phi = kwargs.pop("phi", 90)
    self.phi_rad = _np.deg2rad(self.phi)

    self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
    self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
    self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

get_Jr()

Returns Magnetisation vector

Returns:

Type Description
ndarray

[Jx, Jy]

Source code in src/pymagnet/magnets/_magnet2D.py
140
141
142
143
144
145
146
def get_Jr(self):
    """Returns Magnetisation vector

    Returns:
        ndarray: [Jx, Jy]
    """
    return _np.array([self.Jx, self.Jy])

get_field(x, y)

Calculates the magnetic field at point(s) x,y due to a rectangular magnet

Parameters:

Name Type Description Default
x ndarray

x co-ordinates

required
y ndarray

y co-ordinates

required

Returns:

Type Description
tuple

magnetic field vector Bx (ndarray), By (ndarray)

Source code in src/pymagnet/magnets/_magnet2D.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def get_field(self, x, y):
    """Calculates the magnetic field at point(s) x,y due to a rectangular magnet

    Args:
        x (ndarray): x co-ordinates
        y (ndarray): y co-ordinates

    Returns:
        tuple: magnetic field vector Bx (ndarray), By (ndarray)
    """
    from ..utils._routines2D import _get_field_array_shape2, rotate_points_2D

    array_shape = _get_field_array_shape2(x, y)
    Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)

    if _np.fabs(self.alpha_radians) > Magnet2D.tol:
        xi, yi = rotate_points_2D(
            x - self.center[0], y - self.center[1], self.alpha_radians
        )

    # Calculate field due to x-component of magnetisation
    if _np.fabs(self.Jx / self.Jr) > Magnet2D.tol:
        if _np.fabs(self.alpha_radians) > Magnet2D.tol:
            # Calculate fields in local frame
            Btx = self._calcBx_mag_x(xi, yi)
            Bty = self._calcBy_mag_x(xi, yi)

            # Rotate fields to global frame
            Bx, By = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)

        else:
            Bx = self._calcBx_mag_x(x - self.center[0], y - self.center[1])
            By = self._calcBy_mag_x(x - self.center[0], y - self.center[1])

    # Calculate field due to y-component of magnetisation
    if _np.fabs(self.Jy / self.Jr) > Magnet2D.tol:
        if _np.fabs(self.alpha_radians) > Magnet2D.tol:
            Btx = self._calcBx_mag_y(xi, yi)
            Bty = self._calcBy_mag_y(xi, yi)

            Bxt, Byt = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)
            Bx += Bxt
            By += Byt
        else:
            Bx += self._calcBx_mag_y(x - self.center[0], y - self.center[1])
            By += self._calcBy_mag_y(x - self.center[0], y - self.center[1])
    return Bx, By

get_size()

Returns magnet dimesions

Returns:

Type Description
ndarray

numpy array [width, height]

Source code in src/pymagnet/magnets/_magnet2D.py
112
113
114
115
116
117
118
def get_size(self):
    """Returns magnet dimesions

    Returns:
        ndarray: numpy array [width, height]
    """
    return _np.array([self.width, self.height])

Sphere

Bases: Magnet3D

Sphere 3D Magnet Class

Returns:

Type Description
Sphere

Sphere 3D magnet object

Source code in src/pymagnet/magnets/_magnet3D.py
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
class Sphere(Magnet3D):
    """Sphere 3D Magnet Class

    Returns:
        Sphere: Sphere 3D magnet object
    """

    mag_type = "Sphere"

    def __init__(
        self,
        radius=10.0,
        Jr=1.0,  # local magnetisation direction
        **kwargs,
    ):
        """Init Method

        Args:
            radius (float, optional): radius. Defaults to 10.0.
            Jr (float, optional): remnant magnetisation. Defaults to 1.0.

        Kwargs:
            center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
            mask_magnet (bool): Flag to mask magnet or not in plots
            alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0
            beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0
            gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0
        """
        super().__init__(Jr, **kwargs)
        self.radius = radius

        self.phi = kwargs.pop("phi", None)
        self.theta = kwargs.pop("theta", None)

        if self.phi is not None or self.theta is not None:
            print("Warning, the magnetisation of a sphere is always in z.")
            print("Do not use phi or theta.")
            print("To rotate the magnetisation, use alpha, beta and gamma")

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def get_size(self):
        """Returns magnet dimesions

        Returns:
            size[ndarray]: numpy array [radius, length]
        """
        return _np.array([self.radius])

    def get_Jr(self):
        return _np.array([0, 0, self.Jr])

    def get_force_torque(self, num_samples=100, unit="mm"):
        """Calculates the force and torque on a sphere magnet due to all other magnets.

        Args:
            num_samples (int, optional): Number of samples per axis per face. Defaults to 100.
            unit (str, optional): Length scale. Defaults to 'mm'.

        Returns:
            tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
        """
        from ..forces._sphere_force import calc_force_sphere

        force, torque = calc_force_sphere(self, num_samples, unit)
        return force, torque

    def _get_field_internal(self, x, y, z):
        """Internal magnetic field calculation methods.
        Calculates the field due to a spherical magnet magnetised along z
        (in local coordinates). Returns a dipolar field outside the magnet

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates

        Returns:
            Vector3: Magnetic field array
        """
        from ..utils._conversions import cart2sph, sphere_sph2cart
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)

        # Convert to spherical coordinates
        r, theta, phi = cart2sph(x, y, z)

        # Calculates field for sphere magnetised along z
        Br, Btheta = self._calcB_spherical(r, theta)

        # Convert magnetic fields from spherical to cartesian
        B.x, B.y, B.z = sphere_sph2cart(Br, Btheta, theta, phi)
        return B

    def _calcB_spherical(self, r, theta):
        """Calculates the magnetic field due to due to a sphere at any point
        in spherical coordinates

        Args:
            r (float/array): radial coordinates
            theta (float/array): azimuthal coordinates

        Returns:
            tuple: Br, Btheta
        """

        # Hide the warning for situtations where there is a divide by zero.
        # This returns a NaN in the array, which is ignored for plotting.
        with _np.errstate(divide="ignore", invalid="ignore"):
            preFac = self.Jr * (self.radius**3 / r**3) / 3.0

        Br = preFac * 2.0 * _np.cos(theta)
        Btheta = preFac * _np.sin(theta)

        return Br, Btheta

    def _generate_mask(self, x, y, z):
        """Generates mask of points inside a spherical magnet

        Args:
            x (ndarray/float): x-coordinates
            y (ndarray/float): y-coordinates
            z (ndarray/float): z-coordinates
        """

        data_norm = x**2 + y**2 + z**2
        mask = data_norm < self.radius**2

        return mask

__init__(radius=10.0, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
radius float

radius. Defaults to 10.0.

10.0
Jr float

remnant magnetisation. Defaults to 1.0.

1.0
Kwargs

center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0) mask_magnet (bool): Flag to mask magnet or not in plots alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0 beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0 gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0

Source code in src/pymagnet/magnets/_magnet3D.py
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
def __init__(
    self,
    radius=10.0,
    Jr=1.0,  # local magnetisation direction
    **kwargs,
):
    """Init Method

    Args:
        radius (float, optional): radius. Defaults to 10.0.
        Jr (float, optional): remnant magnetisation. Defaults to 1.0.

    Kwargs:
        center (ndarray): Magnet center. Defaults to (0.0, 0.0, 0.0)
        mask_magnet (bool): Flag to mask magnet or not in plots
        alpha (float): Magnet Orientation angle about z (degrees). Defaults to 0.0
        beta (float): Magnet Orientation angle about y (degrees). Defaults to 0.0
        gamma (float): Magnet Orientation angle about x (degrees). Defaults to 0.0
    """
    super().__init__(Jr, **kwargs)
    self.radius = radius

    self.phi = kwargs.pop("phi", None)
    self.theta = kwargs.pop("theta", None)

    if self.phi is not None or self.theta is not None:
        print("Warning, the magnetisation of a sphere is always in z.")
        print("Do not use phi or theta.")
        print("To rotate the magnetisation, use alpha, beta and gamma")

get_force_torque(num_samples=100, unit='mm')

Calculates the force and torque on a sphere magnet due to all other magnets.

Parameters:

Name Type Description Default
num_samples int

Number of samples per axis per face. Defaults to 100.

100
unit str

Length scale. Defaults to 'mm'.

'mm'

Returns:

Type Description
tuple

force (ndarray (3,) ) and torque (ndarray (3,) )

Source code in src/pymagnet/magnets/_magnet3D.py
913
914
915
916
917
918
919
920
921
922
923
924
925
926
def get_force_torque(self, num_samples=100, unit="mm"):
    """Calculates the force and torque on a sphere magnet due to all other magnets.

    Args:
        num_samples (int, optional): Number of samples per axis per face. Defaults to 100.
        unit (str, optional): Length scale. Defaults to 'mm'.

    Returns:
        tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
    """
    from ..forces._sphere_force import calc_force_sphere

    force, torque = calc_force_sphere(self, num_samples, unit)
    return force, torque

get_size()

Returns magnet dimesions

Returns:

Type Description
size[ndarray]

numpy array [radius, length]

Source code in src/pymagnet/magnets/_magnet3D.py
902
903
904
905
906
907
908
def get_size(self):
    """Returns magnet dimesions

    Returns:
        size[ndarray]: numpy array [radius, length]
    """
    return _np.array([self.radius])

Square

Bases: Rectangle

Square 2D Magnet Class

Source code in src/pymagnet/magnets/_magnet2D.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
class Square(Rectangle):
    """Square 2D Magnet Class"""

    mag_type = "Square"

    def __init__(self, width=20, Jr=1.0, **kwargs):
        """Init Method

        Args:
            width (float, optional): Square side length. Defaults to 20.0.
            Jr (float, optional): Remnant Magnetisation. Defaults to 1.0.

        Kwargs:
             alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
            center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
            phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
        """
        super().__init__(width=width, height=width, Jr=Jr, **kwargs)

__init__(width=20, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
width float

Square side length. Defaults to 20.0.

20
Jr float

Remnant Magnetisation. Defaults to 1.0.

1.0
Kwargs

alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.

center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
Source code in src/pymagnet/magnets/_magnet2D.py
283
284
285
286
287
288
289
290
291
292
293
294
295
def __init__(self, width=20, Jr=1.0, **kwargs):
    """Init Method

    Args:
        width (float, optional): Square side length. Defaults to 20.0.
        Jr (float, optional): Remnant Magnetisation. Defaults to 1.0.

    Kwargs:
         alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
        center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
        phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
    """
    super().__init__(width=width, height=width, Jr=Jr, **kwargs)

get_total_field_mesh(meshes, x, y, z, r_cut=_np.inf)

Compute the total magnetic field from multiple Mesh magnets in one pass.

Concatenates triangle data from all meshes and evaluates the field at every point (x, y, z) using the points-outer parallel kernel _get_field_parallel_pts_njit. An optional distance cutoff skips triangles whose centroid is farther than r_cut from an evaluation point, which can give a large speedup for sparse or localised geometries.

Parameters:

Name Type Description Default
meshes list

list (or any iterable) of Mesh instances. Pass pm.magnets.Mesh.instances to include all currently registered meshes.

required
x float or ndarray

x co-ordinates of the evaluation points.

required
y float or ndarray

y co-ordinates of the evaluation points.

required
z float or ndarray

z co-ordinates of the evaluation points.

required
r_cut float

distance cutoff in the same length units as the mesh coordinates. Triangles farther than r_cut from a point are skipped. Default: np.inf (no culling — full accuracy).

inf

Returns:

Type Description
Field3

total magnetic field array (attributes .x, .y, .z, .n).

Example::

import pymagnet as pm
import numpy as np

pm.reset()
m1 = pm.magnets.Mesh("left.stl",  Jr=1.0, center=[-30, 0, 0])
m2 = pm.magnets.Mesh("right.stl", Jr=1.0, center=[ 30, 0, 0])

x = np.linspace(-60, 60, 40)
X, Y, Z = np.meshgrid(x, x, x, indexing="ij")

# Single fused pass — equivalent to summing m1.get_field() + m2.get_field()
B = pm.magnets.get_total_field_mesh([m1, m2], X, Y, Z, r_cut=40.0)
Source code in src/pymagnet/magnets/_polygon3D.py
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
def get_total_field_mesh(meshes, x, y, z, r_cut=_np.inf):
    """Compute the total magnetic field from multiple Mesh magnets in one pass.

    Concatenates triangle data from all meshes and evaluates the field at
    every point ``(x, y, z)`` using the points-outer parallel kernel
    ``_get_field_parallel_pts_njit``.  An optional distance cutoff skips
    triangles whose centroid is farther than ``r_cut`` from an evaluation
    point, which can give a large speedup for sparse or localised geometries.

    Args:
        meshes (list): list (or any iterable) of ``Mesh`` instances.  Pass
            ``pm.magnets.Mesh.instances`` to include all currently registered
            meshes.
        x (float or ndarray): x co-ordinates of the evaluation points.
        y (float or ndarray): y co-ordinates of the evaluation points.
        z (float or ndarray): z co-ordinates of the evaluation points.
        r_cut (float): distance cutoff in the same length units as the mesh
            coordinates.  Triangles farther than ``r_cut`` from a point are
            skipped.  Default: ``np.inf`` (no culling — full accuracy).

    Returns:
        Field3: total magnetic field array (attributes ``.x``, ``.y``, ``.z``,
            ``.n``).

    Example::

        import pymagnet as pm
        import numpy as np

        pm.reset()
        m1 = pm.magnets.Mesh("left.stl",  Jr=1.0, center=[-30, 0, 0])
        m2 = pm.magnets.Mesh("right.stl", Jr=1.0, center=[ 30, 0, 0])

        x = np.linspace(-60, 60, 40)
        X, Y, Z = np.meshgrid(x, x, x, indexing="ij")

        # Single fused pass — equivalent to summing m1.get_field() + m2.get_field()
        B = pm.magnets.get_total_field_mesh([m1, m2], X, Y, Z, r_cut=40.0)
    """
    from ..utils._routines3D import _allocate_field_array3

    B = _allocate_field_array3(x, y, z)
    vec_shape = B.x.shape

    x_flat = _np.asarray(x).ravel().astype(_np.float64)
    y_flat = _np.asarray(y).ravel().astype(_np.float64)
    z_flat = _np.asarray(z).ravel().astype(_np.float64)

    rotations, offsets, RA_tris1, RA_tris2, swap_flags, active, centroids, Jnorm = (
        _precompute_all_meshes(meshes)
    )

    Bx, By, Bz = _get_field_parallel_pts_njit(
        rotations,
        offsets,
        RA_tris1,
        RA_tris2,
        swap_flags,
        active,
        centroids,
        Jnorm,
        x_flat,
        y_flat,
        z_flat,
        float(r_cut),
    )

    Bx[~_np.isfinite(Bx)] = 0.0
    By[~_np.isfinite(By)] = 0.0
    Bz[~_np.isfinite(Bz)] = 0.0

    B.x = Bx.reshape(vec_shape)
    B.y = By.reshape(vec_shape)
    B.z = Bz.reshape(vec_shape)
    B.n = _np.linalg.norm([B.x, B.y, B.z], axis=0)
    return B

magnetic_field_cylinder_1D(magnet, z)

Calculates the magnetic field z-component due to a cuboid along its axial symmetry center.

Parameters:

Name Type Description Default
magnet Magnet3D

magnet object, Cylinder

required
z ndarray

Array of points along the symmetry axis

required

Returns:

Type Description
Field1

z-component of the magnetic field and associated unit ('T')

Source code in src/pymagnet/magnets/_magnet1D.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def magnetic_field_cylinder_1D(magnet, z):
    """Calculates the magnetic field z-component due to a cuboid along its
    axial symmetry center.

    Args:
        magnet (Magnet3D): magnet object, Cylinder
        z (ndarray): Array of points along the symmetry axis

    Returns:
        Field1: z-component of the magnetic field and associated unit ('T')
    """
    from ._magnet3D import Cylinder

    if issubclass(magnet.__class__, Cylinder):
        L = magnet.length
        R = magnet.radius
        Jr = magnet.Jr

        z_local = _np.asarray(z) - magnet.length / 2 - magnet.center[2]

        zL = z_local + L
        R_sq = _np.power(R, 2)
        z_sq = _np.power(z_local, 2)
        zL_sq = _np.power(zL, 2)

        Bz = (zL / _np.sqrt(zL_sq + R_sq)) - (z_local / _np.sqrt(z_sq + R_sq))
        Bz *= Jr / 2
        data = Field1(Bz)
        return data

    else:
        print(f"Error, the magnet should be a 3D magnet not {magnet.__class__}")
        return None

magnetic_field_prism_1D(magnet, z)

Calculates the magnetic field z-component due to a cuboid along its axial symmetry center.

Parameters:

Name Type Description Default
magnet Magnet3D

magnet object, one of Prism, or Cube

required
z ndarray

Array of points along the symmetry axis

required

Returns:

Type Description
Field1

z-component of the magnetic field and associated unit ('T')

Source code in src/pymagnet/magnets/_magnet1D.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def magnetic_field_prism_1D(magnet, z):
    """Calculates the magnetic field z-component due to a cuboid along its
    axial symmetry center.

    Args:
        magnet (Magnet3D): magnet object, one of Prism, or Cube
        z (ndarray): Array of points along the symmetry axis

    Returns:
        Field1: z-component of the magnetic field and associated unit ('T')
    """
    from ._magnet3D import Prism

    if issubclass(magnet.__class__, Prism):
        a = magnet.a
        b = magnet.b
        c = magnet.c
        Jr = magnet.Jr

        z_local = _np.asarray(z) - c - magnet.center[2]

        ab = a * b
        a_sq = _np.power(a, 2)
        b_sq = _np.power(b, 2)
        z_sq = _np.power(z_local, 2)
        zc = z_local + 2 * c
        zc_sq = _np.power(zc, 2)

        Bz = _np.arctan2(zc * _np.sqrt(a_sq + b_sq + zc_sq), ab) - _np.arctan2(
            z_local * _np.sqrt(a_sq + b_sq + z_sq), ab
        )
        Bz *= Jr / PI
        field = Field1(Bz)
        return field
    else:
        print(f"Error, the magnet should be a 3D magnet not {magnet.__class__}")
        return None

Magnet Base class

This private module implements the registry and base magnet classes

Magnet

Bases: Registry

Magnet base class

Returns:

Type Description
Magnet

magnet base class

Source code in src/pymagnet/magnets/_magnet_base.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class Magnet(Registry):
    """Magnet base class


    Returns:
        Magnet: magnet base class
    """

    tol = MAG_TOL  # tolerance for rotations, sufficient for 0.01 degree accuracy
    mag_type = "Magnet"

    def __init__(self, *args, **kwargs):
        super().__init__()
        self.center = _np.array([0.0, 0.0])

Registry

Registry class for tracking instances

Instances are tracked in class.instances using weak references. This also includes any instances that are deleted manually or go out of scope.

Class methods:

`print_instances()`

`get_instances()`

`get_num_instances()`

`reset()
Source code in src/pymagnet/magnets/_magnet_base.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class Registry:
    """Registry class for tracking instances

    Instances are tracked in `class.instances` using weak references.
    This also includes any instances that are deleted manually or go out of
    scope.

    Class methods:

        `print_instances()`

        `get_instances()`

        `get_num_instances()`

        `reset()
    """

    instances = WeakSet()
    _class_instances = []

    def __new__(cls, *args, **kwargs):
        o = object.__new__(cls)
        cls._register_instance(o)
        return o

    def __init__(self, *args, **kwargs) -> None:
        super().__init__()
        self.__class__._class_instances.append(self)

    def __del__(self) -> None:
        if self in self.__class__._class_instances:
            self.__class__._class_instances.remove(self)

    @classmethod
    def print_instances(cls):
        """Prints class instantiations"""
        if len(cls.instances) < 1:
            print("No Instances")
        else:
            for instance in cls.instances:
                print(instance)

    @classmethod
    def get_instances(cls):
        """Gets lists of instances

        Returns:
            set: all class instances
        """
        return cls.instances

    @classmethod
    def get_num_instances(cls, Print_Val=False):
        """Return number of instances of class

        Args:
            Print_Val (bool, optional): [Print to screen]. Defaults to False.

        Returns:
            num_instances [int]:
        """
        if Print_Val:
            print(len(cls.instances))
        return len(cls.instances)

    @classmethod
    def _register_instance(cls, instance):
        """Adds class instance to registry

        Args:
            instance (instance): class instance
        """
        cls.instances.add(instance)
        for b in cls.__bases__:
            if issubclass(b, Registry):
                b._register_instance(instance)

    @classmethod
    def reset(cls):
        """Removes all instances from registry."""
        for magnet in cls._class_instances:
            del magnet
        cls.instances = WeakSet()
        cls._class_instances = []

    def __init_subclass__(cls):
        cls.instances = WeakSet()
        cls._class_instances = []

get_instances() classmethod

Gets lists of instances

Returns:

Type Description
set

all class instances

Source code in src/pymagnet/magnets/_magnet_base.py
62
63
64
65
66
67
68
69
@classmethod
def get_instances(cls):
    """Gets lists of instances

    Returns:
        set: all class instances
    """
    return cls.instances

get_num_instances(Print_Val=False) classmethod

Return number of instances of class

Parameters:

Name Type Description Default
Print_Val bool

[Print to screen]. Defaults to False.

False

Returns:

Type Description
num_instances[int]
Source code in src/pymagnet/magnets/_magnet_base.py
71
72
73
74
75
76
77
78
79
80
81
82
83
@classmethod
def get_num_instances(cls, Print_Val=False):
    """Return number of instances of class

    Args:
        Print_Val (bool, optional): [Print to screen]. Defaults to False.

    Returns:
        num_instances [int]:
    """
    if Print_Val:
        print(len(cls.instances))
    return len(cls.instances)

print_instances() classmethod

Prints class instantiations

Source code in src/pymagnet/magnets/_magnet_base.py
53
54
55
56
57
58
59
60
@classmethod
def print_instances(cls):
    """Prints class instantiations"""
    if len(cls.instances) < 1:
        print("No Instances")
    else:
        for instance in cls.instances:
            print(instance)

reset() classmethod

Removes all instances from registry.

Source code in src/pymagnet/magnets/_magnet_base.py
 97
 98
 99
100
101
102
103
@classmethod
def reset(cls):
    """Removes all instances from registry."""
    for magnet in cls._class_instances:
        del magnet
    cls.instances = WeakSet()
    cls._class_instances = []

list()

Returns a list of all instantiated magnets.

Assumes that the child class registries have not been modified outside of using pymagnet.reset().

Source code in src/pymagnet/magnets/_magnet_base.py
152
153
154
155
156
157
158
def list():
    """Returns a list of all instantiated magnets.

    Assumes that the child class registries have not been modified outside of
    using `pymagnet.reset()`.
    """
    return Magnet.print_instances()

reset()

Clears the instance registry of every magnet class.

Source code in src/pymagnet/magnets/_magnet_base.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def reset():
    """Clears the instance registry of every magnet class."""
    from ._magnet2D import Circle, Magnet2D, Rectangle, Square
    from ._magnet3D import Cube, Cylinder, Magnet3D, Prism, Sphere
    from ._polygon2D import PolyMagnet
    from ._polygon3D import Mesh

    magnet_classes = [
        # Registry,
        Magnet,
        Magnet2D,
        Rectangle,
        Square,
        Circle,
        PolyMagnet,
        Magnet3D,
        Prism,
        Cube,
        Cylinder,
        Sphere,
        Mesh,
    ]
    for cls in magnet_classes:
        cls.reset()

1D High symmetry methods

This private module implements magnetic field calculations in z along the symmetry centre of a cylindrical or cuboidal magnet.

magnetic_field_cylinder_1D(magnet, z)

Calculates the magnetic field z-component due to a cuboid along its axial symmetry center.

Parameters:

Name Type Description Default
magnet Magnet3D

magnet object, Cylinder

required
z ndarray

Array of points along the symmetry axis

required

Returns:

Type Description
Field1

z-component of the magnetic field and associated unit ('T')

Source code in src/pymagnet/magnets/_magnet1D.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def magnetic_field_cylinder_1D(magnet, z):
    """Calculates the magnetic field z-component due to a cuboid along its
    axial symmetry center.

    Args:
        magnet (Magnet3D): magnet object, Cylinder
        z (ndarray): Array of points along the symmetry axis

    Returns:
        Field1: z-component of the magnetic field and associated unit ('T')
    """
    from ._magnet3D import Cylinder

    if issubclass(magnet.__class__, Cylinder):
        L = magnet.length
        R = magnet.radius
        Jr = magnet.Jr

        z_local = _np.asarray(z) - magnet.length / 2 - magnet.center[2]

        zL = z_local + L
        R_sq = _np.power(R, 2)
        z_sq = _np.power(z_local, 2)
        zL_sq = _np.power(zL, 2)

        Bz = (zL / _np.sqrt(zL_sq + R_sq)) - (z_local / _np.sqrt(z_sq + R_sq))
        Bz *= Jr / 2
        data = Field1(Bz)
        return data

    else:
        print(f"Error, the magnet should be a 3D magnet not {magnet.__class__}")
        return None

magnetic_field_prism_1D(magnet, z)

Calculates the magnetic field z-component due to a cuboid along its axial symmetry center.

Parameters:

Name Type Description Default
magnet Magnet3D

magnet object, one of Prism, or Cube

required
z ndarray

Array of points along the symmetry axis

required

Returns:

Type Description
Field1

z-component of the magnetic field and associated unit ('T')

Source code in src/pymagnet/magnets/_magnet1D.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def magnetic_field_prism_1D(magnet, z):
    """Calculates the magnetic field z-component due to a cuboid along its
    axial symmetry center.

    Args:
        magnet (Magnet3D): magnet object, one of Prism, or Cube
        z (ndarray): Array of points along the symmetry axis

    Returns:
        Field1: z-component of the magnetic field and associated unit ('T')
    """
    from ._magnet3D import Prism

    if issubclass(magnet.__class__, Prism):
        a = magnet.a
        b = magnet.b
        c = magnet.c
        Jr = magnet.Jr

        z_local = _np.asarray(z) - c - magnet.center[2]

        ab = a * b
        a_sq = _np.power(a, 2)
        b_sq = _np.power(b, 2)
        z_sq = _np.power(z_local, 2)
        zc = z_local + 2 * c
        zc_sq = _np.power(zc, 2)

        Bz = _np.arctan2(zc * _np.sqrt(a_sq + b_sq + zc_sq), ab) - _np.arctan2(
            z_local * _np.sqrt(a_sq + b_sq + z_sq), ab
        )
        Bz *= Jr / PI
        field = Field1(Bz)
        return field
    else:
        print(f"Error, the magnet should be a 3D magnet not {magnet.__class__}")
        return None

2D Magnet Classes

This private module implements the Rectangle and Square and Circle 2D magnet classes. The parent class Magnet2D implements the location and orientation methods, i.e. magnet center and quaternion methods for rotating the magnet with respect to each principal axis.

Circle

Bases: Magnet2D

Circle 2D Magnet Class

Source code in src/pymagnet/magnets/_magnet2D.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
class Circle(Magnet2D):
    """Circle 2D Magnet Class"""

    mag_type = "Circle"

    def __init__(
        self,
        radius=10,
        Jr=1.0,  # local magnetisation
        **kwargs,
    ):
        """Init Method

        Args:
            radius (float, optional): Radius. Defaults to 10.0.
            Jr (float, optional): Remnant magnetisation. Defaults to 1.0.

        Kwargs:
            alpha (float): Unused. For rotations use phi instead
            center (tuple or ndarray): magnet center (x, y). Defaults to (0,0)
            phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
        """
        super().__init__(Jr, **kwargs)
        self.radius = radius
        self.phi = kwargs.pop("phi", 0)
        self.phi_rad = _np.deg2rad(self.phi)

        self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
        self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
        self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

        self.center = kwargs.pop("center", _np.array([0.0, 0.0]))
        self.center = _np.asarray(self.center)

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation: alpha {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation: alpha {self.get_orientation()}\n"
        )
        return str

    def get_size(self):
        """Returns radius

        Returns:
            ndarray: radius
        """
        return _np.array([self.radius])

    def get_Jr(self):
        """Returns signed remnant magnetisation

        Returns:
            ndarray: remnant magnetisation
        """
        return _np.array([self.Jx, self.Jy])

    def get_field(self, x, y):
        """Calculates the magnetic field due to long bipolar cylinder

        Args:
            x (ndarray): x coordinates
            y (ndarray): y coordinates

        Returns:
            tuple: Bx, By magnetic field in cartesian coordinates
        """
        from ..utils._conversions import cart2pol, vector_pol2cart
        from ..utils._routines2D import rotate_points_2D

        if _np.fabs(self.alpha_radians) > Magnet2D.tol:
            xi, yi = rotate_points_2D(
                x - self.center[0], y - self.center[1], self.alpha_radians
            )

            rho, phi = cart2pol(xi, yi)
            Brho, Bphi = self._calcB_polar(rho, phi - self.phi_rad)

            # Convert magnetic fields from cylindrical to cartesian
            Bx, By = vector_pol2cart(Brho, Bphi, phi)
            Bx, By = rotate_points_2D(Bx, By, 2 * PI - self.alpha_radians)
            return Bx, By

        rho, phi = cart2pol(x - self.center[0], y - self.center[1])

        Brho, Bphi = self._calcB_polar(rho, phi - self.phi_rad)

        # Convert magnetic fields from cylindrical to cartesian
        Bx, By = vector_pol2cart(Brho, Bphi, phi)

        return Bx, By

    def _calcB_polar(self, rho, phi):
        """Calculates the magnetic field due to long bipolar cylinder in polar
        coordinates

        Args:
            rho (ndarray): radial values
            phi (ndarray): azimuthal values

        Returns:
            tuple: Br, Bphi magnetic field in polar coordinates
        """
        prefac = self.Jr * (self.radius**2 / rho**2) / 2

        Brho = prefac * _np.cos(phi)
        Bphi = prefac * _np.sin(phi)

        return Brho, Bphi

__init__(radius=10, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
radius float

Radius. Defaults to 10.0.

10
Jr float

Remnant magnetisation. Defaults to 1.0.

1.0
Kwargs

alpha (float): Unused. For rotations use phi instead center (tuple or ndarray): magnet center (x, y). Defaults to (0,0) phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.

Source code in src/pymagnet/magnets/_magnet2D.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def __init__(
    self,
    radius=10,
    Jr=1.0,  # local magnetisation
    **kwargs,
):
    """Init Method

    Args:
        radius (float, optional): Radius. Defaults to 10.0.
        Jr (float, optional): Remnant magnetisation. Defaults to 1.0.

    Kwargs:
        alpha (float): Unused. For rotations use phi instead
        center (tuple or ndarray): magnet center (x, y). Defaults to (0,0)
        phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
    """
    super().__init__(Jr, **kwargs)
    self.radius = radius
    self.phi = kwargs.pop("phi", 0)
    self.phi_rad = _np.deg2rad(self.phi)

    self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
    self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
    self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

    self.center = kwargs.pop("center", _np.array([0.0, 0.0]))
    self.center = _np.asarray(self.center)

get_Jr()

Returns signed remnant magnetisation

Returns:

Type Description
ndarray

remnant magnetisation

Source code in src/pymagnet/magnets/_magnet2D.py
360
361
362
363
364
365
366
def get_Jr(self):
    """Returns signed remnant magnetisation

    Returns:
        ndarray: remnant magnetisation
    """
    return _np.array([self.Jx, self.Jy])

get_field(x, y)

Calculates the magnetic field due to long bipolar cylinder

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required

Returns:

Type Description
tuple

Bx, By magnetic field in cartesian coordinates

Source code in src/pymagnet/magnets/_magnet2D.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def get_field(self, x, y):
    """Calculates the magnetic field due to long bipolar cylinder

    Args:
        x (ndarray): x coordinates
        y (ndarray): y coordinates

    Returns:
        tuple: Bx, By magnetic field in cartesian coordinates
    """
    from ..utils._conversions import cart2pol, vector_pol2cart
    from ..utils._routines2D import rotate_points_2D

    if _np.fabs(self.alpha_radians) > Magnet2D.tol:
        xi, yi = rotate_points_2D(
            x - self.center[0], y - self.center[1], self.alpha_radians
        )

        rho, phi = cart2pol(xi, yi)
        Brho, Bphi = self._calcB_polar(rho, phi - self.phi_rad)

        # Convert magnetic fields from cylindrical to cartesian
        Bx, By = vector_pol2cart(Brho, Bphi, phi)
        Bx, By = rotate_points_2D(Bx, By, 2 * PI - self.alpha_radians)
        return Bx, By

    rho, phi = cart2pol(x - self.center[0], y - self.center[1])

    Brho, Bphi = self._calcB_polar(rho, phi - self.phi_rad)

    # Convert magnetic fields from cylindrical to cartesian
    Bx, By = vector_pol2cart(Brho, Bphi, phi)

    return Bx, By

get_size()

Returns radius

Returns:

Type Description
ndarray

radius

Source code in src/pymagnet/magnets/_magnet2D.py
352
353
354
355
356
357
358
def get_size(self):
    """Returns radius

    Returns:
        ndarray: radius
    """
    return _np.array([self.radius])

Magnet2D

Bases: Magnet

2D Magnet Base Class

Source code in src/pymagnet/magnets/_magnet2D.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class Magnet2D(Magnet):
    """2D Magnet Base Class"""

    mag_type = "Magnet2D"

    def __init__(self, Jr, **kwargs) -> None:
        """Init Method

        Args:
            Jr (float): signed magnetised of remnant magnetisation

        Kwargs:
            alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
            center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
        """
        super().__init__()
        self.Jr = Jr

        # Magnet rotation w.r.t. x-axis
        self.alpha = kwargs.pop("alpha", 0.0)
        self.alpha_radians = _np.deg2rad(self.alpha)

        self.center = kwargs.pop("center", _np.array([0.0, 0.0]))
        self.center = _np.asarray(self.center)

    def get_center(self):
        """Returns magnet centre

        Returns:
            center (ndarray): numpy array [xc, yc]
        """
        return self.center

    def get_orientation(self):
        """Returns magnet orientation, `alpha` in degrees

        Returns:
            float: alpha, rotation angle w.r.t x-axis.
        """

        return self.alpha

    def get_field(self, x, y) -> None:
        """Calculates the magnetic field.

        This is a template that needs to be implemented for each magnet

        Args:
            x (ndarray): x co-ordinates
            y (ndarray): y co-ordinates
        """
        pass

    def get_force_torque(self) -> None:
        """Calculates the force and torque on a magnet due to all other magnets.

        This is a template that needs to be implemented for each magnet.
        """
        pass

__init__(Jr, **kwargs)

Init Method

Parameters:

Name Type Description Default
Jr float

signed magnetised of remnant magnetisation

required
Kwargs

alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0. center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).

Source code in src/pymagnet/magnets/_magnet2D.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(self, Jr, **kwargs) -> None:
    """Init Method

    Args:
        Jr (float): signed magnetised of remnant magnetisation

    Kwargs:
        alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
        center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
    """
    super().__init__()
    self.Jr = Jr

    # Magnet rotation w.r.t. x-axis
    self.alpha = kwargs.pop("alpha", 0.0)
    self.alpha_radians = _np.deg2rad(self.alpha)

    self.center = kwargs.pop("center", _np.array([0.0, 0.0]))
    self.center = _np.asarray(self.center)

get_center()

Returns magnet centre

Returns:

Type Description
center (ndarray

numpy array [xc, yc]

Source code in src/pymagnet/magnets/_magnet2D.py
44
45
46
47
48
49
50
def get_center(self):
    """Returns magnet centre

    Returns:
        center (ndarray): numpy array [xc, yc]
    """
    return self.center

get_field(x, y)

Calculates the magnetic field.

This is a template that needs to be implemented for each magnet

Parameters:

Name Type Description Default
x ndarray

x co-ordinates

required
y ndarray

y co-ordinates

required
Source code in src/pymagnet/magnets/_magnet2D.py
61
62
63
64
65
66
67
68
69
70
def get_field(self, x, y) -> None:
    """Calculates the magnetic field.

    This is a template that needs to be implemented for each magnet

    Args:
        x (ndarray): x co-ordinates
        y (ndarray): y co-ordinates
    """
    pass

get_force_torque()

Calculates the force and torque on a magnet due to all other magnets.

This is a template that needs to be implemented for each magnet.

Source code in src/pymagnet/magnets/_magnet2D.py
72
73
74
75
76
77
def get_force_torque(self) -> None:
    """Calculates the force and torque on a magnet due to all other magnets.

    This is a template that needs to be implemented for each magnet.
    """
    pass

get_orientation()

Returns magnet orientation, alpha in degrees

Returns:

Type Description
float

alpha, rotation angle w.r.t x-axis.

Source code in src/pymagnet/magnets/_magnet2D.py
52
53
54
55
56
57
58
59
def get_orientation(self):
    """Returns magnet orientation, `alpha` in degrees

    Returns:
        float: alpha, rotation angle w.r.t x-axis.
    """

    return self.alpha

Rectangle

Bases: Magnet2D

Rectangular 2D Magnet Class

Source code in src/pymagnet/magnets/_magnet2D.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
class Rectangle(Magnet2D):
    """Rectangular 2D Magnet Class"""

    mag_type = "Rectangle"

    def __init__(self, width=20.0, height=40.0, Jr=1.0, **kwargs):
        """Init Method

        Args:
            width (float, optional): Magnet Width. Defaults to 20.0.
            height (float, optional): Magnet Height. Defaults to 40.0.
            Jr (float, optional): Remnant Magnetisation. Defaults to 1.0.

        Kwargs:
            alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
            center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
            phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
        """
        super().__init__(Jr, **kwargs)
        self.width = width
        self.height = height

        self.a = width / 2
        self.b = height / 2

        self.phi = kwargs.pop("phi", 90)
        self.phi_rad = _np.deg2rad(self.phi)

        self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
        self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
        self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

    def get_size(self):
        """Returns magnet dimesions

        Returns:
            ndarray: numpy array [width, height]
        """
        return _np.array([self.width, self.height])

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation: alpha {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Size: {self.get_size()}\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation: alpha {self.get_orientation()}\n"
        )
        return str

    def get_Jr(self):
        """Returns Magnetisation vector

        Returns:
            ndarray: [Jx, Jy]
        """
        return _np.array([self.Jx, self.Jy])

    def get_field(self, x, y):
        """Calculates the magnetic field at point(s) x,y due to a rectangular magnet

        Args:
            x (ndarray): x co-ordinates
            y (ndarray): y co-ordinates

        Returns:
            tuple: magnetic field vector Bx (ndarray), By (ndarray)
        """
        from ..utils._routines2D import _get_field_array_shape2, rotate_points_2D

        array_shape = _get_field_array_shape2(x, y)
        Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)

        if _np.fabs(self.alpha_radians) > Magnet2D.tol:
            xi, yi = rotate_points_2D(
                x - self.center[0], y - self.center[1], self.alpha_radians
            )

        # Calculate field due to x-component of magnetisation
        if _np.fabs(self.Jx / self.Jr) > Magnet2D.tol:
            if _np.fabs(self.alpha_radians) > Magnet2D.tol:
                # Calculate fields in local frame
                Btx = self._calcBx_mag_x(xi, yi)
                Bty = self._calcBy_mag_x(xi, yi)

                # Rotate fields to global frame
                Bx, By = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)

            else:
                Bx = self._calcBx_mag_x(x - self.center[0], y - self.center[1])
                By = self._calcBy_mag_x(x - self.center[0], y - self.center[1])

        # Calculate field due to y-component of magnetisation
        if _np.fabs(self.Jy / self.Jr) > Magnet2D.tol:
            if _np.fabs(self.alpha_radians) > Magnet2D.tol:
                Btx = self._calcBx_mag_y(xi, yi)
                Bty = self._calcBy_mag_y(xi, yi)

                Bxt, Byt = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)
                Bx += Bxt
                By += Byt
            else:
                Bx += self._calcBx_mag_y(x - self.center[0], y - self.center[1])
                By += self._calcBy_mag_y(x - self.center[0], y - self.center[1])
        return Bx, By

    def _calcBx_mag_x(self, x, y):
        """Bx using 2D Model for rectangular sheets magnetised in x-plane

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            ndarray: Bx, x component of magnetic field
        """
        a = self.a
        b = self.b
        J = self.Jx
        # Hide the warning for situtations where there is a divide by zero.
        # This returns a NaN in the array, which is ignored for plotting.
        with _np.errstate(divide="ignore", invalid="ignore"):
            return (J / (2 * PI)) * (
                _np.arctan2((2 * a * (b + y)), (x**2 - a**2 + (y + b) ** 2))
                + _np.arctan2((2 * a * (b - y)), (x**2 - a**2 + (y - b) ** 2))
            )

    def _calcBy_mag_x(self, x, y):
        """By using 2D Model for rectangular sheets magnetised in x-plane

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            ndarray: By, x component of magnetic field
        """
        a = self.a
        b = self.b
        J = self.Jx
        # Hide the warning for situtations where there is a divide by zero.
        # This returns a NaN in the array, which is ignored for plotting.
        with _np.errstate(divide="ignore", invalid="ignore"):
            return (-J / (4 * PI)) * (
                _np.log(((x - a) ** 2 + (y - b) ** 2) / ((x + a) ** 2 + (y - b) ** 2))
                - _np.log(((x - a) ** 2 + (y + b) ** 2) / ((x + a) ** 2 + (y + b) ** 2))
            )

    def _calcBx_mag_y(self, x, y):
        """Bx using 2D Model for rectangular sheets magnetised in y-plane

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            ndarray: Bx, x component of magnetic field
        """
        a = self.a
        b = self.b
        J = self.Jy
        # Hide the warning for situtations where there is a divide by zero.
        # This returns a NaN in the array, which is ignored for plotting.
        with _np.errstate(divide="ignore", invalid="ignore"):
            return (J / (4 * PI)) * (
                _np.log(((x + a) ** 2 + (y - b) ** 2) / ((x + a) ** 2 + (y + b) ** 2))
                - _np.log(((x - a) ** 2 + (y - b) ** 2) / ((x - a) ** 2 + (y + b) ** 2))
            )

    def _calcBy_mag_y(self, x: float, y: float) -> float:
        """Bx using 2D Model for rectangular sheets magnetised in y-plane

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            ndarray: By, x component of magnetic field
        """
        a = self.a
        b = self.b
        J = self.Jy
        return (J / (2 * PI)) * (
            _np.arctan2((2 * b * (x + a)), ((x + a) ** 2 + y**2 - b**2))
            - _np.arctan2((2 * b * (x - a)), ((x - a) ** 2 + y**2 - b**2))
        )

__init__(width=20.0, height=40.0, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
width float

Magnet Width. Defaults to 20.0.

20.0
height float

Magnet Height. Defaults to 40.0.

40.0
Jr float

Remnant Magnetisation. Defaults to 1.0.

1.0
Kwargs

alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0. center (tuple or ndarray): magnet center (x, y). Defaults to (0,0). phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.

Source code in src/pymagnet/magnets/_magnet2D.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def __init__(self, width=20.0, height=40.0, Jr=1.0, **kwargs):
    """Init Method

    Args:
        width (float, optional): Magnet Width. Defaults to 20.0.
        height (float, optional): Magnet Height. Defaults to 40.0.
        Jr (float, optional): Remnant Magnetisation. Defaults to 1.0.

    Kwargs:
        alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
        center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
        phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
    """
    super().__init__(Jr, **kwargs)
    self.width = width
    self.height = height

    self.a = width / 2
    self.b = height / 2

    self.phi = kwargs.pop("phi", 90)
    self.phi_rad = _np.deg2rad(self.phi)

    self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
    self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
    self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy

get_Jr()

Returns Magnetisation vector

Returns:

Type Description
ndarray

[Jx, Jy]

Source code in src/pymagnet/magnets/_magnet2D.py
140
141
142
143
144
145
146
def get_Jr(self):
    """Returns Magnetisation vector

    Returns:
        ndarray: [Jx, Jy]
    """
    return _np.array([self.Jx, self.Jy])

get_field(x, y)

Calculates the magnetic field at point(s) x,y due to a rectangular magnet

Parameters:

Name Type Description Default
x ndarray

x co-ordinates

required
y ndarray

y co-ordinates

required

Returns:

Type Description
tuple

magnetic field vector Bx (ndarray), By (ndarray)

Source code in src/pymagnet/magnets/_magnet2D.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def get_field(self, x, y):
    """Calculates the magnetic field at point(s) x,y due to a rectangular magnet

    Args:
        x (ndarray): x co-ordinates
        y (ndarray): y co-ordinates

    Returns:
        tuple: magnetic field vector Bx (ndarray), By (ndarray)
    """
    from ..utils._routines2D import _get_field_array_shape2, rotate_points_2D

    array_shape = _get_field_array_shape2(x, y)
    Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)

    if _np.fabs(self.alpha_radians) > Magnet2D.tol:
        xi, yi = rotate_points_2D(
            x - self.center[0], y - self.center[1], self.alpha_radians
        )

    # Calculate field due to x-component of magnetisation
    if _np.fabs(self.Jx / self.Jr) > Magnet2D.tol:
        if _np.fabs(self.alpha_radians) > Magnet2D.tol:
            # Calculate fields in local frame
            Btx = self._calcBx_mag_x(xi, yi)
            Bty = self._calcBy_mag_x(xi, yi)

            # Rotate fields to global frame
            Bx, By = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)

        else:
            Bx = self._calcBx_mag_x(x - self.center[0], y - self.center[1])
            By = self._calcBy_mag_x(x - self.center[0], y - self.center[1])

    # Calculate field due to y-component of magnetisation
    if _np.fabs(self.Jy / self.Jr) > Magnet2D.tol:
        if _np.fabs(self.alpha_radians) > Magnet2D.tol:
            Btx = self._calcBx_mag_y(xi, yi)
            Bty = self._calcBy_mag_y(xi, yi)

            Bxt, Byt = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)
            Bx += Bxt
            By += Byt
        else:
            Bx += self._calcBx_mag_y(x - self.center[0], y - self.center[1])
            By += self._calcBy_mag_y(x - self.center[0], y - self.center[1])
    return Bx, By

get_size()

Returns magnet dimesions

Returns:

Type Description
ndarray

numpy array [width, height]

Source code in src/pymagnet/magnets/_magnet2D.py
112
113
114
115
116
117
118
def get_size(self):
    """Returns magnet dimesions

    Returns:
        ndarray: numpy array [width, height]
    """
    return _np.array([self.width, self.height])

Square

Bases: Rectangle

Square 2D Magnet Class

Source code in src/pymagnet/magnets/_magnet2D.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
class Square(Rectangle):
    """Square 2D Magnet Class"""

    mag_type = "Square"

    def __init__(self, width=20, Jr=1.0, **kwargs):
        """Init Method

        Args:
            width (float, optional): Square side length. Defaults to 20.0.
            Jr (float, optional): Remnant Magnetisation. Defaults to 1.0.

        Kwargs:
             alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
            center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
            phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
        """
        super().__init__(width=width, height=width, Jr=Jr, **kwargs)

__init__(width=20, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
width float

Square side length. Defaults to 20.0.

20
Jr float

Remnant Magnetisation. Defaults to 1.0.

1.0
Kwargs

alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.

center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
Source code in src/pymagnet/magnets/_magnet2D.py
283
284
285
286
287
288
289
290
291
292
293
294
295
def __init__(self, width=20, Jr=1.0, **kwargs):
    """Init Method

    Args:
        width (float, optional): Square side length. Defaults to 20.0.
        Jr (float, optional): Remnant Magnetisation. Defaults to 1.0.

    Kwargs:
         alpha (float): Magnetisation orientation angle (in degrees). Defaults to 0.
        center (tuple or ndarray): magnet center (x, y). Defaults to (0,0).
        phi (float): Rotation Angle (in degrees) of magnet w.r.t x-axis. Defaults to 90.
    """
    super().__init__(width=width, height=width, Jr=Jr, **kwargs)

2D Polygon Magnet class

Line

Line Class for storing properties of a sheet manget

Source code in src/pymagnet/magnets/_polygon2D.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
class Line:
    """Line Class for storing properties of a sheet manget"""

    def __init__(self, length, center, beta, K):
        """Init Method

        Args:
            length (float): side length
            center (ndarray): magnet center (x, y)
            beta (float): Orientation w.r.t. z-axis in degrees
            K (float): Sheet current density in tesla
        """
        self.length = length
        self.center = center
        self.beta = beta
        self.beta_rad = _np.deg2rad(beta)
        self.xc = center[0]
        self.yc = center[1]
        self.K = K
        self.tol = MAG_TOL

    def __str__(self):
        str = (
            f"K: {self.K} (T)\n"
            + f"Length: {self.length} (m)\n"
            + f"Center {self.center} (m)\n"
            + f"Orientation: {self.beta}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"K: {self.K} (T)\n"
            + f"Length: {self.length}\n"
            + f"Center {self.center}\n"
            + f"Orientation: {self.beta}\n"
        )
        return str

    def get_center(self):
        """Returns line center

        Returns:
            ndarray: center (x,y)
        """

        return self.center

    def get_field(self, x, y):
        """Calculates the magnetic field due to a sheet magnet
        First a transformation into the local coordinates is made, the field calculated
        and then the magnetic field it rotated out to the global coordinates

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            tuple: Bx (ndarray), By (ndarray) magnetic field vector
        """
        from ..utils._routines2D import _get_field_array_shape2, rotate_points_2D

        array_shape = _get_field_array_shape2(x, y)
        Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)
        if _np.fabs(self.beta_rad) > self.tol:
            xt, yt = rotate_points_2D(x - self.xc, y - self.yc, 2 * PI - self.beta_rad)
            Btx, Bty = _sheet_field(xt, yt, self.length / 2, self.K)
            Bx, By = rotate_points_2D(Btx, Bty, self.beta_rad)

        else:
            Bx, By = _sheet_field(x - self.xc, y - self.yc, self.length / 2, self.K)
        return Bx, By

__init__(length, center, beta, K)

Init Method

Parameters:

Name Type Description Default
length float

side length

required
center ndarray

magnet center (x, y)

required
beta float

Orientation w.r.t. z-axis in degrees

required
K float

Sheet current density in tesla

required
Source code in src/pymagnet/magnets/_polygon2D.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def __init__(self, length, center, beta, K):
    """Init Method

    Args:
        length (float): side length
        center (ndarray): magnet center (x, y)
        beta (float): Orientation w.r.t. z-axis in degrees
        K (float): Sheet current density in tesla
    """
    self.length = length
    self.center = center
    self.beta = beta
    self.beta_rad = _np.deg2rad(beta)
    self.xc = center[0]
    self.yc = center[1]
    self.K = K
    self.tol = MAG_TOL

get_center()

Returns line center

Returns:

Type Description
ndarray

center (x,y)

Source code in src/pymagnet/magnets/_polygon2D.py
299
300
301
302
303
304
305
306
def get_center(self):
    """Returns line center

    Returns:
        ndarray: center (x,y)
    """

    return self.center

get_field(x, y)

Calculates the magnetic field due to a sheet magnet First a transformation into the local coordinates is made, the field calculated and then the magnetic field it rotated out to the global coordinates

Parameters:

Name Type Description Default
x ndarray

x-coordinates

required
y ndarray

y-coordinates

required

Returns:

Type Description
tuple

Bx (ndarray), By (ndarray) magnetic field vector

Source code in src/pymagnet/magnets/_polygon2D.py
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def get_field(self, x, y):
    """Calculates the magnetic field due to a sheet magnet
    First a transformation into the local coordinates is made, the field calculated
    and then the magnetic field it rotated out to the global coordinates

    Args:
        x (ndarray): x-coordinates
        y (ndarray): y-coordinates

    Returns:
        tuple: Bx (ndarray), By (ndarray) magnetic field vector
    """
    from ..utils._routines2D import _get_field_array_shape2, rotate_points_2D

    array_shape = _get_field_array_shape2(x, y)
    Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)
    if _np.fabs(self.beta_rad) > self.tol:
        xt, yt = rotate_points_2D(x - self.xc, y - self.yc, 2 * PI - self.beta_rad)
        Btx, Bty = _sheet_field(xt, yt, self.length / 2, self.K)
        Bx, By = rotate_points_2D(Btx, Bty, self.beta_rad)

    else:
        Bx, By = _sheet_field(x - self.xc, y - self.yc, self.length / 2, self.K)
    return Bx, By

LineUtils

Utility class consisting of rountines for 2D line elements

Source code in src/pymagnet/magnets/_polygon2D.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
class LineUtils:
    """Utility class consisting of rountines for 2D line elements"""

    @staticmethod
    def unit_norm(vertex_1, vertex_2, clockwise=True):
        """Get unit normal to vertex

        Args:
            vertex_1 (ndarray): vertex 1
            vertex_2 (ndarray): vertex 2
            clockwise (bool, optional): Clockwise orientation of points.
                Defaults to True.

        Returns:
            tuple: normal vector (ndarray), length i.e. distance between
                vertices (float)
        """

        dx = vertex_1[0] - vertex_2[0]
        dy = vertex_1[1] - vertex_2[1]

        # Clockwise winding of points:
        if clockwise:
            norm = _np.array([dy, -dx])
        else:
            norm = _np.array([-dy, dx])
        length = _np.linalg.norm(norm)
        norm = norm / length
        return norm, length

    @staticmethod
    def line_center(vertex_1, vertex_2):
        """Gets midpoint of two vertices

        Args:
            vertex_1 (ndarray): vertex 1
            vertex_2 (ndarray): vertex 2

        Returns:
            ndarray: midpoint
        """
        xc = (vertex_1[0] + vertex_2[0]) / 2
        yc = (vertex_1[1] + vertex_2[1]) / 2

        return _np.array([xc, yc])

    @staticmethod
    def signed_area2D(polygon):
        """Calculates signed area of a polygon

        Args:
            polygon (Polygon): Polygon instance

        Returns:
            float: signed area
        """
        j = 1
        NP = polygon.num_vertices()
        area = 0
        norm = _np.empty([NP, 2])
        center = _np.empty([NP, 2])
        beta = _np.empty(NP)  # angle w.r.t. y axis
        length = _np.empty(NP)

        for i in range(NP):
            j = j % NP
            area += (polygon.vertices[j][0] - polygon.vertices[i][0]) * (
                polygon.vertices[j][1] + polygon.vertices[i][1]
            )
            norm[i, :], length[i] = LineUtils.unit_norm(
                polygon.vertices[i], polygon.vertices[j]
            )
            center[i, :] = LineUtils.line_center(
                polygon.vertices[i], polygon.vertices[j]
            )
            j += 1

        # check winding order of polygon, area < 0 for clockwise ordering of points
        if area < 0:
            norm *= -1
        beta[:] = _np.rad2deg(_np.arctan2(norm[:, 1], norm[:, 0]))

        return area / 2.0, norm, beta, length, center

line_center(vertex_1, vertex_2) staticmethod

Gets midpoint of two vertices

Parameters:

Name Type Description Default
vertex_1 ndarray

vertex 1

required
vertex_2 ndarray

vertex 2

required

Returns:

Type Description
ndarray

midpoint

Source code in src/pymagnet/magnets/_polygon2D.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
@staticmethod
def line_center(vertex_1, vertex_2):
    """Gets midpoint of two vertices

    Args:
        vertex_1 (ndarray): vertex 1
        vertex_2 (ndarray): vertex 2

    Returns:
        ndarray: midpoint
    """
    xc = (vertex_1[0] + vertex_2[0]) / 2
    yc = (vertex_1[1] + vertex_2[1]) / 2

    return _np.array([xc, yc])

signed_area2D(polygon) staticmethod

Calculates signed area of a polygon

Parameters:

Name Type Description Default
polygon Polygon

Polygon instance

required

Returns:

Type Description
float

signed area

Source code in src/pymagnet/magnets/_polygon2D.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
@staticmethod
def signed_area2D(polygon):
    """Calculates signed area of a polygon

    Args:
        polygon (Polygon): Polygon instance

    Returns:
        float: signed area
    """
    j = 1
    NP = polygon.num_vertices()
    area = 0
    norm = _np.empty([NP, 2])
    center = _np.empty([NP, 2])
    beta = _np.empty(NP)  # angle w.r.t. y axis
    length = _np.empty(NP)

    for i in range(NP):
        j = j % NP
        area += (polygon.vertices[j][0] - polygon.vertices[i][0]) * (
            polygon.vertices[j][1] + polygon.vertices[i][1]
        )
        norm[i, :], length[i] = LineUtils.unit_norm(
            polygon.vertices[i], polygon.vertices[j]
        )
        center[i, :] = LineUtils.line_center(
            polygon.vertices[i], polygon.vertices[j]
        )
        j += 1

    # check winding order of polygon, area < 0 for clockwise ordering of points
    if area < 0:
        norm *= -1
    beta[:] = _np.rad2deg(_np.arctan2(norm[:, 1], norm[:, 0]))

    return area / 2.0, norm, beta, length, center

unit_norm(vertex_1, vertex_2, clockwise=True) staticmethod

Get unit normal to vertex

Parameters:

Name Type Description Default
vertex_1 ndarray

vertex 1

required
vertex_2 ndarray

vertex 2

required
clockwise bool

Clockwise orientation of points. Defaults to True.

True

Returns:

Type Description
tuple

normal vector (ndarray), length i.e. distance between vertices (float)

Source code in src/pymagnet/magnets/_polygon2D.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@staticmethod
def unit_norm(vertex_1, vertex_2, clockwise=True):
    """Get unit normal to vertex

    Args:
        vertex_1 (ndarray): vertex 1
        vertex_2 (ndarray): vertex 2
        clockwise (bool, optional): Clockwise orientation of points.
            Defaults to True.

    Returns:
        tuple: normal vector (ndarray), length i.e. distance between
            vertices (float)
    """

    dx = vertex_1[0] - vertex_2[0]
    dy = vertex_1[1] - vertex_2[1]

    # Clockwise winding of points:
    if clockwise:
        norm = _np.array([dy, -dx])
    else:
        norm = _np.array([-dy, dx])
    length = _np.linalg.norm(norm)
    norm = norm / length
    return norm, length

PolyMagnet

Bases: Magnet2D

2D Magnet Polygon class.

Source code in src/pymagnet/magnets/_polygon2D.py
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
class PolyMagnet(Magnet2D):
    """2D Magnet Polygon class."""

    mag_type = "PolyMagnet"

    def __init__(self, Jr, **kwargs) -> None:
        """Init method

        NOTE:
            * When creating a regular polygon, one of apothem, radius, or length
              must be defined as a kwarg or an exception will be raised.
            * When creating a regular polygon, the number of sides `num_sides`
            must be at least 3 or an exception will be raised.
            * When creating a custom polygon at least one vertex pair must be
            defined with `vertices` or an exception will be raised.

        Args:
            Jr (float): signed magnitude of remnant magnetisation

        Kwargs:
            alpha (float): Not used
            theta (float): Orientation of magnet w.r.t x-axis of magnet
            phi (float): Orientation of magnetisation w.r.t x-axis of magnet in degrees.
            Defaults to 90.0.
            center (ndarray): magnet center (x, y). Defaults to (0.0, 0.0)
            length (float): side length if creating a regular polygon
            apothem (float): apothem (incircle radius) if creating a regular polygon
            radius (float): radius (circumcircle radius) if  creating a regular polygon
            num_sides (int): number of sides of a regular polygon. Defaults to 6.
            custom_polygon (bool): Flag to define a custom polygon. Defaults to False.
            vertices (ndarray, list): List of custom vertices. Defaults to None.

        Raises:
            Exception: If creating a custom polygon, `vertices` must not be None.
        """
        from ..utils._routines2D import rotate_points_2D

        super().__init__(Jr, **kwargs)

        # Magnet rotation w.r.t. x-axis
        self.alpha = kwargs.pop("alpha", 0.0)
        self.alpha_radians = _np.deg2rad(self.alpha)

        self.theta = kwargs.pop("theta", 0.0)
        self.theta_radians = _np.deg2rad(self.theta)

        self.phi = kwargs.pop("phi", 90.0)
        self.phi_rad = _np.deg2rad(self.phi)

        self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
        self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
        self.tol = MAG_TOL
        self.area = None

        self.custom_polygon = kwargs.pop("custom_polygon", False)

        self.center = kwargs.pop("center", _np.array([0.0, 0.0, 0.0]))
        self.center = _np.asarray(self.center)

        if self.custom_polygon:
            vertices = kwargs.pop("vertices", None)
            if vertices is None:
                raise ValueError("Error, no vertices were defined.")

            vertices = _np.atleast_2d(vertices)

            x_rot, y_rot = rotate_points_2D(
                vertices[:, 0],
                vertices[:, 1],
                self.theta_radians,  # + self.alpha_radians,
            )
            vertices = _np.stack([x_rot, y_rot]).T + self.center
            self.polygon = Polygon(vertices=vertices.tolist())
        else:
            self.length = kwargs.pop("length", None)
            self.apothem = kwargs.pop("apothem", None)
            self.radius = kwargs.pop("radius", None)
            self.num_sides = kwargs.pop("num_sides", 6)

            self.radius = Polygon.check_radius(
                self.num_sides,
                self.apothem,
                self.length,
                self.radius,
            )
            # Generate Polygon
            self.polygon = Polygon(
                vertices=Polygon.gen_polygon(
                    self.num_sides,
                    self.center,
                    self.theta,  # + self.alpha,
                    length=self.length,
                    apothem=self.apothem,
                    radius=self.radius,
                ),
                center=self.center,
            )

    def get_center(self):
        """Returns magnet centre

        Returns:
            center (ndarray): numpy array [xc, yc]
        """
        return self.center

    def get_orientation(self):
        """Returns magnet orientation, `alpha` in degrees

        Returns:
            float: alpha, rotation angle w.r.t x-axis.
        """

        return self.alpha

    def _gen_sheet_magnets(self):
        """Generates orientation, size, and centre of sheet magnets for a given
        polygon

        Returns:
            tuple: beta (ndarray), length (ndarray), centre (ndarray),
            K (ndarray) - sheet current density in tesla.
        """
        area, norms, beta, length, center = LineUtils.signed_area2D(self.polygon)
        K = self.Jx * norms[:, 1] - self.Jy * norms[:, 0]
        self.area = area
        return beta, length, center, K

    def get_field(self, x, y):
        """Calculates the magnetic field of a polygon due to each face

        Args:
            x (ndarray): x-coordinates
            y (ndarray): y-coordinates

        Returns:
            tuple: Bx (ndarray), By (ndarray) magnetic field vector
        """
        from ..utils._routines2D import _get_field_array_shape2

        array_shape = _get_field_array_shape2(x, y)
        Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)
        beta, length, center, K = self._gen_sheet_magnets()

        if _np.fabs(self.alpha_radians) > self.tol:
            pass
            print("Arbitrary rotation with alpha not yet implemented!!")

            # FIXME: rotate centres
            # xt, yt = rotate_points_2D(x - self.xc, y - self.yc, self.alpha_radians)
            # beta += self.alpha
            # xc_rot, yc_rot = rotate_points_2D(
            #     center[:, 0] - self.xc,
            #     center[:, 1] - self.yc,
            #     self.alpha_radians,
            # )
            # center[:, 0] = xc_rot
            # center[:, 1] = yc_rot
            #
            #
            # for i in range(len(K)):
            #     sheet = Line(length[i], center[i], beta[i], K[i])
            #     Btx, Bty = sheet.get_field(xt, yt)
            #     Btx, Bty = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)
            #     Bx += Btx
            #     By += Bty

        for i in range(len(K)):
            sheet = Line(length[i], center[i], beta[i], K[i])
            Btx, Bty = sheet.get_field(x, y)
            Bx += Btx
            By += Bty
        return Bx, By

__init__(Jr, **kwargs)

Init method

NOTE
  • When creating a regular polygon, one of apothem, radius, or length must be defined as a kwarg or an exception will be raised.
  • When creating a regular polygon, the number of sides num_sides must be at least 3 or an exception will be raised.
  • When creating a custom polygon at least one vertex pair must be defined with vertices or an exception will be raised.

Parameters:

Name Type Description Default
Jr float

signed magnitude of remnant magnetisation

required
Kwargs

alpha (float): Not used theta (float): Orientation of magnet w.r.t x-axis of magnet phi (float): Orientation of magnetisation w.r.t x-axis of magnet in degrees. Defaults to 90.0. center (ndarray): magnet center (x, y). Defaults to (0.0, 0.0) length (float): side length if creating a regular polygon apothem (float): apothem (incircle radius) if creating a regular polygon radius (float): radius (circumcircle radius) if creating a regular polygon num_sides (int): number of sides of a regular polygon. Defaults to 6. custom_polygon (bool): Flag to define a custom polygon. Defaults to False. vertices (ndarray, list): List of custom vertices. Defaults to None.

Raises:

Type Description
Exception

If creating a custom polygon, vertices must not be None.

Source code in src/pymagnet/magnets/_polygon2D.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def __init__(self, Jr, **kwargs) -> None:
    """Init method

    NOTE:
        * When creating a regular polygon, one of apothem, radius, or length
          must be defined as a kwarg or an exception will be raised.
        * When creating a regular polygon, the number of sides `num_sides`
        must be at least 3 or an exception will be raised.
        * When creating a custom polygon at least one vertex pair must be
        defined with `vertices` or an exception will be raised.

    Args:
        Jr (float): signed magnitude of remnant magnetisation

    Kwargs:
        alpha (float): Not used
        theta (float): Orientation of magnet w.r.t x-axis of magnet
        phi (float): Orientation of magnetisation w.r.t x-axis of magnet in degrees.
        Defaults to 90.0.
        center (ndarray): magnet center (x, y). Defaults to (0.0, 0.0)
        length (float): side length if creating a regular polygon
        apothem (float): apothem (incircle radius) if creating a regular polygon
        radius (float): radius (circumcircle radius) if  creating a regular polygon
        num_sides (int): number of sides of a regular polygon. Defaults to 6.
        custom_polygon (bool): Flag to define a custom polygon. Defaults to False.
        vertices (ndarray, list): List of custom vertices. Defaults to None.

    Raises:
        Exception: If creating a custom polygon, `vertices` must not be None.
    """
    from ..utils._routines2D import rotate_points_2D

    super().__init__(Jr, **kwargs)

    # Magnet rotation w.r.t. x-axis
    self.alpha = kwargs.pop("alpha", 0.0)
    self.alpha_radians = _np.deg2rad(self.alpha)

    self.theta = kwargs.pop("theta", 0.0)
    self.theta_radians = _np.deg2rad(self.theta)

    self.phi = kwargs.pop("phi", 90.0)
    self.phi_rad = _np.deg2rad(self.phi)

    self.Jx = _np.around(Jr * _np.cos(self.phi_rad), decimals=6)
    self.Jy = _np.around(Jr * _np.sin(self.phi_rad), decimals=6)
    self.tol = MAG_TOL
    self.area = None

    self.custom_polygon = kwargs.pop("custom_polygon", False)

    self.center = kwargs.pop("center", _np.array([0.0, 0.0, 0.0]))
    self.center = _np.asarray(self.center)

    if self.custom_polygon:
        vertices = kwargs.pop("vertices", None)
        if vertices is None:
            raise ValueError("Error, no vertices were defined.")

        vertices = _np.atleast_2d(vertices)

        x_rot, y_rot = rotate_points_2D(
            vertices[:, 0],
            vertices[:, 1],
            self.theta_radians,  # + self.alpha_radians,
        )
        vertices = _np.stack([x_rot, y_rot]).T + self.center
        self.polygon = Polygon(vertices=vertices.tolist())
    else:
        self.length = kwargs.pop("length", None)
        self.apothem = kwargs.pop("apothem", None)
        self.radius = kwargs.pop("radius", None)
        self.num_sides = kwargs.pop("num_sides", 6)

        self.radius = Polygon.check_radius(
            self.num_sides,
            self.apothem,
            self.length,
            self.radius,
        )
        # Generate Polygon
        self.polygon = Polygon(
            vertices=Polygon.gen_polygon(
                self.num_sides,
                self.center,
                self.theta,  # + self.alpha,
                length=self.length,
                apothem=self.apothem,
                radius=self.radius,
            ),
            center=self.center,
        )

get_center()

Returns magnet centre

Returns:

Type Description
center (ndarray

numpy array [xc, yc]

Source code in src/pymagnet/magnets/_polygon2D.py
432
433
434
435
436
437
438
def get_center(self):
    """Returns magnet centre

    Returns:
        center (ndarray): numpy array [xc, yc]
    """
    return self.center

get_field(x, y)

Calculates the magnetic field of a polygon due to each face

Parameters:

Name Type Description Default
x ndarray

x-coordinates

required
y ndarray

y-coordinates

required

Returns:

Type Description
tuple

Bx (ndarray), By (ndarray) magnetic field vector

Source code in src/pymagnet/magnets/_polygon2D.py
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
def get_field(self, x, y):
    """Calculates the magnetic field of a polygon due to each face

    Args:
        x (ndarray): x-coordinates
        y (ndarray): y-coordinates

    Returns:
        tuple: Bx (ndarray), By (ndarray) magnetic field vector
    """
    from ..utils._routines2D import _get_field_array_shape2

    array_shape = _get_field_array_shape2(x, y)
    Bx, By = _np.zeros(array_shape), _np.zeros(array_shape)
    beta, length, center, K = self._gen_sheet_magnets()

    if _np.fabs(self.alpha_radians) > self.tol:
        pass
        print("Arbitrary rotation with alpha not yet implemented!!")

        # FIXME: rotate centres
        # xt, yt = rotate_points_2D(x - self.xc, y - self.yc, self.alpha_radians)
        # beta += self.alpha
        # xc_rot, yc_rot = rotate_points_2D(
        #     center[:, 0] - self.xc,
        #     center[:, 1] - self.yc,
        #     self.alpha_radians,
        # )
        # center[:, 0] = xc_rot
        # center[:, 1] = yc_rot
        #
        #
        # for i in range(len(K)):
        #     sheet = Line(length[i], center[i], beta[i], K[i])
        #     Btx, Bty = sheet.get_field(xt, yt)
        #     Btx, Bty = rotate_points_2D(Btx, Bty, 2 * PI - self.alpha_radians)
        #     Bx += Btx
        #     By += Bty

    for i in range(len(K)):
        sheet = Line(length[i], center[i], beta[i], K[i])
        Btx, Bty = sheet.get_field(x, y)
        Bx += Btx
        By += Bty
    return Bx, By

get_orientation()

Returns magnet orientation, alpha in degrees

Returns:

Type Description
float

alpha, rotation angle w.r.t x-axis.

Source code in src/pymagnet/magnets/_polygon2D.py
440
441
442
443
444
445
446
447
def get_orientation(self):
    """Returns magnet orientation, `alpha` in degrees

    Returns:
        float: alpha, rotation angle w.r.t x-axis.
    """

    return self.alpha

Polygon

Polygon class for generating list of vertices

Source code in src/pymagnet/magnets/_polygon2D.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
class Polygon:
    """Polygon class for generating list of vertices"""

    def __init__(self, **kwargs):
        vertices = kwargs.pop("vertices", None)
        center = kwargs.pop("center", None)
        if vertices is not None:
            if type(vertices) is _np.ndarray:
                if center is not None:
                    vertices += center
                self.vertices = vertices.tolist()
            else:
                self.vertices = vertices

            if center is not None:
                self.center = center
            else:
                self.set_center()
        else:
            self.vertices = []
            self.center = _np.nan

    def append(self, vertex):
        """Appends vertex to list of vertices

        Args:
            vertex (list): list of vertices
        """
        if len(vertex) != 2:
            print("Error")
        if type(vertex) is tuple:
            self.vertices.append(vertex)
        elif len(vertex) == 2:
            self.vertices.append(tuple(vertex))
        self.set_center()

    def num_vertices(self):
        """Gets number of vertices

        Returns:
            int: number of vertices
        """
        return len(self.vertices)

    def set_center(self):
        """Sets center of polygon to be centroid"""
        # FIXME: This is not the correct method!!! It should be the weighted mean
        self.center = _np.mean(_np.asarray(self.vertices), axis=0)

    @staticmethod
    def get_centroid_area(vertex_array):
        sumCx = 0
        sumCy = 0
        sumAc = 0
        for i in range(len(vertex_array) - 1):
            cX = (vertex_array[i][0] + vertex_array[i + 1][0]) * (
                vertex_array[i][0] * vertex_array[i + 1][1]
                - vertex_array[i + 1][0] * vertex_array[i][1]
            )
            cY = (vertex_array[i][1] + vertex_array[i + 1][1]) * (
                vertex_array[i][0] * vertex_array[i + 1][1]
                - vertex_array[i + 1][0] * vertex_array[i][1]
            )
            pA = (vertex_array[i][0] * vertex_array[i + 1][1]) - (
                vertex_array[i + 1][0] * vertex_array[i][1]
            )
            sumCx += cX
            sumCy += cY
            sumAc += pA
        area = sumAc / 2.0
        center = ((1.0 / (6.0 * area)) * sumCx, (1.0 / (6.0 * area)) * sumCy)
        return center, area

    @staticmethod
    def gen_polygon(N=6, center=(0.0, 0.0), alpha=0.0, **kwargs):
        """Generates regular polygon. One of apothem, side length or radius must
        be defined.

        Args:
            N (int, optional): Number of sides. Defaults to 6.
            center (tuple, optional): Polygon center. Defaults to (0.0, 0.0).
            alpha (float, optional): Orientration with respect to x-axis.
                Defaults to 0.0.

        Raises:
            Exception: N must be > 2

        Returns:
            ndarray: polygon vertices
        """
        N = int(N)

        if N < 3:
            raise ValueError("Error, N must be > 2.")

        apothem = kwargs.pop("apothem", None)
        length = kwargs.pop("length", None)
        radius = kwargs.pop("radius", None)

        radius = Polygon.check_radius(N, apothem, length, radius)

        k = _np.arange(0, N, 1)
        xc = center[0]
        yc = center[1]

        def f(N):
            if N % 2 == 0:
                return PI / N + _np.deg2rad(alpha)
            else:
                return PI / N + PI + _np.deg2rad(alpha)

        xv = xc + radius * _np.sin(2 * PI * k / N + f(N))
        yv = yc + radius * _np.cos(2 * PI * k / N + f(N))
        poly_verts = _np.vstack((xv, yv)).T.tolist()

        return poly_verts

    @staticmethod
    def check_radius(N, apothem, length, radius):
        """Checks which of apothem, side length, or radius has been passed as kwargs
        to `gen_polygon()`. Order of precendence is apothem, length, radius.

        Args:
            N (int): Number of sides
            apothem (float): polygon apothem
            length (float): side length
            radius (float): outcircle radius

        Raises:
            Exception: One of apothem, length, or raduis must be defined

        Returns:
            float: returns radius
        """
        if apothem is not None:
            return apothem / _np.around(_np.cos(PI / N), 4)
        elif length is not None:
            return length / _np.around(2 * _np.sin(PI / N), 4)
        elif radius is not None:
            return radius
        else:
            raise ValueError(
                "Error, one of apothem, length, or radius must be defined."
            )

append(vertex)

Appends vertex to list of vertices

Parameters:

Name Type Description Default
vertex list

list of vertices

required
Source code in src/pymagnet/magnets/_polygon2D.py
51
52
53
54
55
56
57
58
59
60
61
62
63
def append(self, vertex):
    """Appends vertex to list of vertices

    Args:
        vertex (list): list of vertices
    """
    if len(vertex) != 2:
        print("Error")
    if type(vertex) is tuple:
        self.vertices.append(vertex)
    elif len(vertex) == 2:
        self.vertices.append(tuple(vertex))
    self.set_center()

check_radius(N, apothem, length, radius) staticmethod

Checks which of apothem, side length, or radius has been passed as kwargs to gen_polygon(). Order of precendence is apothem, length, radius.

Parameters:

Name Type Description Default
N int

Number of sides

required
apothem float

polygon apothem

required
length float

side length

required
radius float

outcircle radius

required

Raises:

Type Description
Exception

One of apothem, length, or raduis must be defined

Returns:

Type Description
float

returns radius

Source code in src/pymagnet/magnets/_polygon2D.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
@staticmethod
def check_radius(N, apothem, length, radius):
    """Checks which of apothem, side length, or radius has been passed as kwargs
    to `gen_polygon()`. Order of precendence is apothem, length, radius.

    Args:
        N (int): Number of sides
        apothem (float): polygon apothem
        length (float): side length
        radius (float): outcircle radius

    Raises:
        Exception: One of apothem, length, or raduis must be defined

    Returns:
        float: returns radius
    """
    if apothem is not None:
        return apothem / _np.around(_np.cos(PI / N), 4)
    elif length is not None:
        return length / _np.around(2 * _np.sin(PI / N), 4)
    elif radius is not None:
        return radius
    else:
        raise ValueError(
            "Error, one of apothem, length, or radius must be defined."
        )

gen_polygon(N=6, center=(0.0, 0.0), alpha=0.0, **kwargs) staticmethod

Generates regular polygon. One of apothem, side length or radius must be defined.

Parameters:

Name Type Description Default
N int

Number of sides. Defaults to 6.

6
center tuple

Polygon center. Defaults to (0.0, 0.0).

(0.0, 0.0)
alpha float

Orientration with respect to x-axis. Defaults to 0.0.

0.0

Raises:

Type Description
Exception

N must be > 2

Returns:

Type Description
ndarray

polygon vertices

Source code in src/pymagnet/magnets/_polygon2D.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
@staticmethod
def gen_polygon(N=6, center=(0.0, 0.0), alpha=0.0, **kwargs):
    """Generates regular polygon. One of apothem, side length or radius must
    be defined.

    Args:
        N (int, optional): Number of sides. Defaults to 6.
        center (tuple, optional): Polygon center. Defaults to (0.0, 0.0).
        alpha (float, optional): Orientration with respect to x-axis.
            Defaults to 0.0.

    Raises:
        Exception: N must be > 2

    Returns:
        ndarray: polygon vertices
    """
    N = int(N)

    if N < 3:
        raise ValueError("Error, N must be > 2.")

    apothem = kwargs.pop("apothem", None)
    length = kwargs.pop("length", None)
    radius = kwargs.pop("radius", None)

    radius = Polygon.check_radius(N, apothem, length, radius)

    k = _np.arange(0, N, 1)
    xc = center[0]
    yc = center[1]

    def f(N):
        if N % 2 == 0:
            return PI / N + _np.deg2rad(alpha)
        else:
            return PI / N + PI + _np.deg2rad(alpha)

    xv = xc + radius * _np.sin(2 * PI * k / N + f(N))
    yv = yc + radius * _np.cos(2 * PI * k / N + f(N))
    poly_verts = _np.vstack((xv, yv)).T.tolist()

    return poly_verts

num_vertices()

Gets number of vertices

Returns:

Type Description
int

number of vertices

Source code in src/pymagnet/magnets/_polygon2D.py
65
66
67
68
69
70
71
def num_vertices(self):
    """Gets number of vertices

    Returns:
        int: number of vertices
    """
    return len(self.vertices)

set_center()

Sets center of polygon to be centroid

Source code in src/pymagnet/magnets/_polygon2D.py
73
74
75
76
def set_center(self):
    """Sets center of polygon to be centroid"""
    # FIXME: This is not the correct method!!! It should be the weighted mean
    self.center = _np.mean(_np.asarray(self.vertices), axis=0)

Mesh

Bases: Magnet3D

Mesh Magnet Class.

Source code in src/pymagnet/magnets/_polygon3D.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
class Mesh(Magnet3D):
    """Mesh Magnet Class."""

    mag_type = "Mesh"

    def __init__(
        self,
        filename,
        Jr=1.0,  # local magnetisation
        **kwargs,
    ):
        """Init Method

        Args:
            filename (string): path to stl file to be imported
            Jr (float, optional): Signed remnant magnetisation. Defaults to 1.0.

        Kwargs:
            phi (float):
            theta (float):
            mesh_scale (float): scaling factor if mesh needs to be resized. Defaults to 1.0
        """
        super().__init__(Jr, **kwargs)

        self.phi = kwargs.pop("phi", 90.0)
        self.phi_rad = _np.deg2rad(self.phi)
        self.theta = kwargs.pop("theta", 0.0)
        self.theta_rad = _np.deg2rad(self.theta)

        self.mesh_scale = kwargs.pop("mesh_scale", 1.0)
        self._filename = filename

        (
            self.mesh_vectors,
            self.mesh_normals,
            self.volume,
            self.centroid,
        ) = self._import_mesh()

        self.Jx = _np.around(
            Jr * _np.cos(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
        )
        self.Jy = _np.around(
            Jr * _np.sin(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
        )
        self.Jz = _np.around(Jr * _np.cos(self.theta_rad), decimals=6)
        self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy
        self.J = _np.array([self.Jx, self.Jy, self.Jz])

        # FIXME: Sort out rotation of magnetisation with rotation of mesh
        # if _np.any(
        #     _np.fabs([self.alpha_rad, self.beta_rad, self.gamma_rad]) > ALIGN_CUTOFF
        # ):
        #     mag_rotation = Quaternion.gen_rotation_quaternion(
        #         self.alpha_rad, self.beta_rad, self.gamma_rad
        #     )
        #     Jrot = mag_rotation * self.J
        #     self.Jx = Jrot[0]
        #     self.Jy = Jrot[1]
        #     self.Jz = Jrot[2]
        #     self.J = _np.array([self.Jx, self.Jy, self.Jz])

        self.Jnorm = _np.dot(self.J, self.mesh_normals.T)

    def __str__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def __repr__(self):
        str = (
            f"{self.__class__.mag_type}\n"
            + f"J: {self.get_Jr()} (T)\n"
            + f"Center {self.get_center()}\n"
            + f"Orientation alpha,beta,gamma: {self.get_orientation()}\n"
        )
        return str

    def get_Jr(self):
        """Returns Magnetisation vector

        Returns:
            ndarray: [Jx, Jy, Jz]
        """
        return self.J

    def size(self):
        """Returns magnet dimesions

        Returns:
            size (ndarray): numpy array [width, depth, height]
        """
        pass

    def get_center(self):
        """Returns magnet center

        Returns:
            ndarray: magnet center
        """
        return self.center

    def get_field(self, x, y, z, parallel=True, r_cut=_np.inf):
        """Calculates the magnetic field at point(s) x,y,z due to a 3D magnet
        The calculations are always performed in local coordinates with the centre of the magnet at origin and z magnetisation pointing along the local z' axis.

        The rotations and translations are performed first, and the internal field calculation functions are called.

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates
            parallel (bool): If True, use parallel numba implementation
            r_cut (float): Distance cutoff. Triangles whose centroid is farther
                than ``r_cut`` from an evaluation point are skipped.  Units must
                match the mesh coordinates (typically mm).  Default: no cutoff.

        Returns:
            tuple: Bx(ndarray), By(ndarray), Bz(ndarray)  field vector
        """
        if parallel:
            B = self._get_field_parallel(x, y, z, r_cut=r_cut)
        else:
            B = self._get_field_internal(x, y, z)

        return B.x, B.y, B.z

    def _get_field_parallel(self, x, y, z, r_cut=_np.inf):
        """Parallel magnetic field calculation using numba.

        Delegates to :meth:`_get_field_parallel_pts`, which parallelises over
        evaluation points (prange outer loop) rather than triangles.  This is
        the only correct parallel strategy: the original triangles-outer kernel
        had a race condition — multiple threads writing to the same output
        array indices without synchronisation — producing non-deterministic
        errors up to ~10% on large meshes.

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates
            r_cut (float): Distance cutoff passed to
                :meth:`_get_field_parallel_pts`.  Default: no cutoff.

        Returns:
            Field3: Magnetic field array
        """
        return self._get_field_parallel_pts(x, y, z, r_cut=r_cut)

    def _get_field_serial_fast(self, x, y, z):
        """Serial but numba-optimized magnetic field calculation.

        Uses the numba-compiled functions but processes triangles serially.
        Useful for comparison with parallel version.

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates

        Returns:
            Field3: Magnetic field array
        """
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)
        vec_shape = B.x.shape

        x_flat = _np.asarray(x).ravel().astype(_np.float64)
        y_flat = _np.asarray(y).ravel().astype(_np.float64)
        z_flat = _np.asarray(z).ravel().astype(_np.float64)

        mesh_vectors = _np.ascontiguousarray(self.mesh_vectors, dtype=_np.float64)
        Jnorm = _np.ascontiguousarray(self.Jnorm, dtype=_np.float64)

        Bx, By, Bz = _get_field_serial_njit(
            mesh_vectors, Jnorm, self.Jr, x_flat, y_flat, z_flat
        )

        Bx[~_np.isfinite(Bx)] = 0.0
        By[~_np.isfinite(By)] = 0.0
        Bz[~_np.isfinite(Bz)] = 0.0

        B.x = Bx.reshape(vec_shape)
        B.y = By.reshape(vec_shape)
        B.z = Bz.reshape(vec_shape)
        B.n = _np.linalg.norm([B.x, B.y, B.z], axis=0)

        return B

    def _get_field_parallel_pts(self, x, y, z, r_cut=_np.inf):
        """Transposed parallel field calculation: prange over evaluation points.

        Precomputes per-triangle rotation data once, then parallelises over
        the evaluation-point axis rather than the triangle axis.  This layout
        enables per-point triangle culling via an optional distance cutoff.

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates
            r_cut (float): distance cutoff in the same units as the mesh
                coordinates.  Triangles whose centroid is farther than r_cut
                from an evaluation point are skipped.  Default: np.inf
                (no culling — full accuracy).

        Returns:
            Field3: Magnetic field array
        """
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)
        vec_shape = B.x.shape

        x_flat = _np.asarray(x).ravel().astype(_np.float64)
        y_flat = _np.asarray(y).ravel().astype(_np.float64)
        z_flat = _np.asarray(z).ravel().astype(_np.float64)

        mesh_vectors = _np.ascontiguousarray(self.mesh_vectors, dtype=_np.float64)
        Jnorm = _np.ascontiguousarray(self.Jnorm, dtype=_np.float64)

        rotations, offsets, RA_tris1, RA_tris2, swap_flags, active, centroids = (
            _precompute_triangle_data(mesh_vectors, Jnorm, self.Jr)
        )

        Bx, By, Bz = _get_field_parallel_pts_njit(
            rotations,
            offsets,
            RA_tris1,
            RA_tris2,
            swap_flags,
            active,
            centroids,
            Jnorm,
            x_flat,
            y_flat,
            z_flat,
            float(r_cut),
        )

        Bx[~_np.isfinite(Bx)] = 0.0
        By[~_np.isfinite(By)] = 0.0
        Bz[~_np.isfinite(Bz)] = 0.0

        B.x = Bx.reshape(vec_shape)
        B.y = By.reshape(vec_shape)
        B.z = Bz.reshape(vec_shape)
        B.n = _np.linalg.norm([B.x, B.y, B.z], axis=0)

        return B

    def get_force_torque(self, depth=4, unit="mm"):
        """Calculates the force and torque on a prism magnet due to all other magnets.

        Args:
            depth (int, optional): Number of recursions of division by 4 per simplex
            unit (str, optional): Length scale. Defaults to 'mm'.

        Returns:
            tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
        """
        from ..forces._mesh_force import calc_force_mesh

        force, torque = calc_force_mesh(self, depth, unit)
        return force, torque

    def _get_field_internal(self, x, y, z):
        """Internal magnetic field calculation methods.
        Iterates over each triangle that makes up the mesh magnet and calculates the magnetic field

        Args:
            x (float/array): x co-ordinates
            y (float/array): y co-ordinates
            z (float/array): z co-ordinates

        Returns:
            Field3: Magnetic field array
        """
        from ..utils._routines3D import _allocate_field_array3

        B = _allocate_field_array3(x, y, z)
        vec_shape = B.x.shape
        B.x = B.x.ravel()
        B.y = B.y.ravel()
        B.z = B.z.ravel()

        # debug for loop, used when needing to check certain triangles, or groups of triangles
        # for i in range(self.start, self.stop):
        for i in range(len(self.mesh_vectors)):
            if _np.fabs(self.Jnorm[i] / self.Jr) > 1e-4:
                Btx, Bty, Btz, _, _, _ = self.calcB_triangle(
                    self.mesh_vectors[i],
                    self.Jnorm[i],
                    x,
                    y,
                    z,
                    i,
                )

                B.x += Btx
                B.y += Bty
                B.z += Btz

        B.x = _np.reshape(B.x, vec_shape)
        B.y = _np.reshape(B.y, vec_shape)
        B.z = _np.reshape(B.z, vec_shape)

        B.n = _np.linalg.norm([B.x, B.y, B.z], axis=0)
        return B

    def _import_mesh(self):
        """Imports mesh from STL file

        Returns:
            tuple: mesh_vectors (ndarray of mesh triangles), mesh_normals (ndarray of normals to each triangle)
        """
        stl_mesh = mesh.Mesh.from_file(self._filename)

        if _np.any(
            _np.fabs([self.alpha_rad, self.beta_rad, self.gamma_rad]) > ALIGN_CUTOFF
        ):
            mesh_rotation = Quaternion.gen_rotation_quaternion(
                self.alpha_rad, self.beta_rad, self.gamma_rad
            )

            angle, axis = mesh_rotation.get_axisangle()
            stl_mesh.rotate(axis, angle)

        # to ensure that the initial center is set to the centroid
        _, centroid, _ = stl_mesh.get_mass_properties()
        stl_mesh.translate(-centroid)

        offset = self.get_center()
        stl_mesh.translate(offset / self.mesh_scale)

        # get values after translation
        volume, centroid, _ = stl_mesh.get_mass_properties()

        mesh_vectors = stl_mesh.vectors.astype(_np.float64)
        mesh_normals = stl_mesh.normals.astype(_np.float64)

        # scale values
        volume *= self.mesh_scale**3
        centroid *= self.mesh_scale
        mesh_vectors *= self.mesh_scale

        mesh_normals = mesh_normals / _np.linalg.norm(
            mesh_normals, axis=1, keepdims=True
        )

        return mesh_vectors, mesh_normals, volume, centroid

    def _generate_mask(self, x, y, z):
        """Generates mask of points inside a magnet
        NOTE: not implemented for Mesh magnets.
        Args:
            x (ndarray/float): x-coordinates
            y (ndarray/float): y-coordinates
            z (ndarray/float): z-coordinates
        """
        pass

    def calcB_triangle(self, triangle, Jr, x, y, z, i):
        """Calculates the magnetic field due to a triangle

        Args:
            triangle (ndarray): Vertices of a triangle
            Jr (float): Remnant magnetisation component normal to triangle
            x (ndarray): x coordinates
            y (ndarray): y coordinates
            z (ndarray): z coordinates

        Returns:
            tuple: Bx, By, Bz magnetic field components
        """

        (
            total_rotation,
            rotated_triangle,
            offset,
            RA_triangle1,
            RA_triangle2,
        ) = _rotate_triangle(triangle, Jr)

        # Prepare points and quaternion
        pos_vec = Quaternion._prepare_vector(x, y, z)

        # Rotate points
        x_rot, y_rot, z_rot = total_rotation * pos_vec

        norm1 = norm_plane(triangle)

        if _np.allclose(norm1, [0, -1, 0], atol=ALIGN_CUTOFF) and Jr < 0:
            RA_triangle1, RA_triangle2 = RA_triangle2, RA_triangle1

        Btx, Bty, Btz = self._calcB_2_triangles(
            RA_triangle1,
            RA_triangle2,
            Jr,
            x_rot - offset[0],
            y_rot - offset[1],
            z_rot - offset[2],
        )

        Bvec = Quaternion._prepare_vector(Btx, Bty, Btz)
        Bx, By, Bz = total_rotation.get_conjugate() * Bvec

        return Bx, By, Bz, rotated_triangle, offset, total_rotation

    def _calcB_2_triangles(self, triangle1, triangle2, Jr, x, y, z):
        """Calculates the magnetic field due to two split right angled triangles
        in their local frame.

        Args:
            triangle1 (ndarray): Vertices of triangle 1
            triangle2 (ndarray): Vertices of triangle 2
            Jr (float): normal remnant magnetisation
            x (ndarray): x coordinates
            y (ndarray): y coordinates
            z (ndarray): z coordinates

        Returns:
            tuple: Bx, By, Bz magnetic field components
        """

        # Calc RA1 Field
        Btx, Bty, Btz = self._charge_sheet(triangle1[0], triangle1[1], Jr, x, y, z)

        # Rotate into local of RA2
        rotate_about_z = q_angle_from_axis(PI, (0, 0, 1))
        pos_vec_RA2 = Quaternion._prepare_vector(x - triangle1[0], y, z)

        x_local, y_local, z_local = rotate_about_z * pos_vec_RA2

        # Calc RA2 Field
        Btx2, Bty2, Btz2 = self._charge_sheet(
            triangle2[0], triangle2[1], Jr, x_local + triangle2[0], y_local, z_local
        )

        # Inverse Rot of RA2 Field
        Bvec = Quaternion._prepare_vector(Btx2, Bty2, Btz2)
        Btx2, Bty2, Btz2 = rotate_about_z.get_conjugate() * Bvec

        Btx += Btx2
        Bty += Bty2
        Btz += Btz2

        return Btx, Bty, Btz

    @staticmethod
    def _charge_sheet(a, b, Jr, x, y, z):
        sigma = Jr
        with _np.errstate(all="ignore"):
            Bx = _charge_sheet_x(a, b, sigma, x, y, z)
            By = _charge_sheet_y(a, b, sigma, x, y, z)
            Bz = _charge_sheet_z(a, b, sigma, x, y, z)
        return Bx, By, Bz

__init__(filename, Jr=1.0, **kwargs)

Init Method

Parameters:

Name Type Description Default
filename string

path to stl file to be imported

required
Jr float

Signed remnant magnetisation. Defaults to 1.0.

1.0
Kwargs

phi (float): theta (float): mesh_scale (float): scaling factor if mesh needs to be resized. Defaults to 1.0

Source code in src/pymagnet/magnets/_polygon3D.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(
    self,
    filename,
    Jr=1.0,  # local magnetisation
    **kwargs,
):
    """Init Method

    Args:
        filename (string): path to stl file to be imported
        Jr (float, optional): Signed remnant magnetisation. Defaults to 1.0.

    Kwargs:
        phi (float):
        theta (float):
        mesh_scale (float): scaling factor if mesh needs to be resized. Defaults to 1.0
    """
    super().__init__(Jr, **kwargs)

    self.phi = kwargs.pop("phi", 90.0)
    self.phi_rad = _np.deg2rad(self.phi)
    self.theta = kwargs.pop("theta", 0.0)
    self.theta_rad = _np.deg2rad(self.theta)

    self.mesh_scale = kwargs.pop("mesh_scale", 1.0)
    self._filename = filename

    (
        self.mesh_vectors,
        self.mesh_normals,
        self.volume,
        self.centroid,
    ) = self._import_mesh()

    self.Jx = _np.around(
        Jr * _np.cos(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
    )
    self.Jy = _np.around(
        Jr * _np.sin(self.phi_rad) * _np.sin(self.theta_rad), decimals=6
    )
    self.Jz = _np.around(Jr * _np.cos(self.theta_rad), decimals=6)
    self.tol = MAG_TOL  # sufficient for 0.01 degree accuracy
    self.J = _np.array([self.Jx, self.Jy, self.Jz])

    # FIXME: Sort out rotation of magnetisation with rotation of mesh
    # if _np.any(
    #     _np.fabs([self.alpha_rad, self.beta_rad, self.gamma_rad]) > ALIGN_CUTOFF
    # ):
    #     mag_rotation = Quaternion.gen_rotation_quaternion(
    #         self.alpha_rad, self.beta_rad, self.gamma_rad
    #     )
    #     Jrot = mag_rotation * self.J
    #     self.Jx = Jrot[0]
    #     self.Jy = Jrot[1]
    #     self.Jz = Jrot[2]
    #     self.J = _np.array([self.Jx, self.Jy, self.Jz])

    self.Jnorm = _np.dot(self.J, self.mesh_normals.T)

calcB_triangle(triangle, Jr, x, y, z, i)

Calculates the magnetic field due to a triangle

Parameters:

Name Type Description Default
triangle ndarray

Vertices of a triangle

required
Jr float

Remnant magnetisation component normal to triangle

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
tuple

Bx, By, Bz magnetic field components

Source code in src/pymagnet/magnets/_polygon3D.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def calcB_triangle(self, triangle, Jr, x, y, z, i):
    """Calculates the magnetic field due to a triangle

    Args:
        triangle (ndarray): Vertices of a triangle
        Jr (float): Remnant magnetisation component normal to triangle
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates

    Returns:
        tuple: Bx, By, Bz magnetic field components
    """

    (
        total_rotation,
        rotated_triangle,
        offset,
        RA_triangle1,
        RA_triangle2,
    ) = _rotate_triangle(triangle, Jr)

    # Prepare points and quaternion
    pos_vec = Quaternion._prepare_vector(x, y, z)

    # Rotate points
    x_rot, y_rot, z_rot = total_rotation * pos_vec

    norm1 = norm_plane(triangle)

    if _np.allclose(norm1, [0, -1, 0], atol=ALIGN_CUTOFF) and Jr < 0:
        RA_triangle1, RA_triangle2 = RA_triangle2, RA_triangle1

    Btx, Bty, Btz = self._calcB_2_triangles(
        RA_triangle1,
        RA_triangle2,
        Jr,
        x_rot - offset[0],
        y_rot - offset[1],
        z_rot - offset[2],
    )

    Bvec = Quaternion._prepare_vector(Btx, Bty, Btz)
    Bx, By, Bz = total_rotation.get_conjugate() * Bvec

    return Bx, By, Bz, rotated_triangle, offset, total_rotation

get_Jr()

Returns Magnetisation vector

Returns:

Type Description
ndarray

[Jx, Jy, Jz]

Source code in src/pymagnet/magnets/_polygon3D.py
107
108
109
110
111
112
113
def get_Jr(self):
    """Returns Magnetisation vector

    Returns:
        ndarray: [Jx, Jy, Jz]
    """
    return self.J

get_center()

Returns magnet center

Returns:

Type Description
ndarray

magnet center

Source code in src/pymagnet/magnets/_polygon3D.py
123
124
125
126
127
128
129
def get_center(self):
    """Returns magnet center

    Returns:
        ndarray: magnet center
    """
    return self.center

get_field(x, y, z, parallel=True, r_cut=_np.inf)

Calculates the magnetic field at point(s) x,y,z due to a 3D magnet The calculations are always performed in local coordinates with the centre of the magnet at origin and z magnetisation pointing along the local z' axis.

The rotations and translations are performed first, and the internal field calculation functions are called.

Parameters:

Name Type Description Default
x float / array

x co-ordinates

required
y float / array

y co-ordinates

required
z float / array

z co-ordinates

required
parallel bool

If True, use parallel numba implementation

True
r_cut float

Distance cutoff. Triangles whose centroid is farther than r_cut from an evaluation point are skipped. Units must match the mesh coordinates (typically mm). Default: no cutoff.

inf

Returns:

Type Description
tuple

Bx(ndarray), By(ndarray), Bz(ndarray) field vector

Source code in src/pymagnet/magnets/_polygon3D.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def get_field(self, x, y, z, parallel=True, r_cut=_np.inf):
    """Calculates the magnetic field at point(s) x,y,z due to a 3D magnet
    The calculations are always performed in local coordinates with the centre of the magnet at origin and z magnetisation pointing along the local z' axis.

    The rotations and translations are performed first, and the internal field calculation functions are called.

    Args:
        x (float/array): x co-ordinates
        y (float/array): y co-ordinates
        z (float/array): z co-ordinates
        parallel (bool): If True, use parallel numba implementation
        r_cut (float): Distance cutoff. Triangles whose centroid is farther
            than ``r_cut`` from an evaluation point are skipped.  Units must
            match the mesh coordinates (typically mm).  Default: no cutoff.

    Returns:
        tuple: Bx(ndarray), By(ndarray), Bz(ndarray)  field vector
    """
    if parallel:
        B = self._get_field_parallel(x, y, z, r_cut=r_cut)
    else:
        B = self._get_field_internal(x, y, z)

    return B.x, B.y, B.z

get_force_torque(depth=4, unit='mm')

Calculates the force and torque on a prism magnet due to all other magnets.

Parameters:

Name Type Description Default
depth int

Number of recursions of division by 4 per simplex

4
unit str

Length scale. Defaults to 'mm'.

'mm'

Returns:

Type Description
tuple

force (ndarray (3,) ) and torque (ndarray (3,) )

Source code in src/pymagnet/magnets/_polygon3D.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def get_force_torque(self, depth=4, unit="mm"):
    """Calculates the force and torque on a prism magnet due to all other magnets.

    Args:
        depth (int, optional): Number of recursions of division by 4 per simplex
        unit (str, optional): Length scale. Defaults to 'mm'.

    Returns:
        tuple: force (ndarray (3,) ) and torque (ndarray (3,) )
    """
    from ..forces._mesh_force import calc_force_mesh

    force, torque = calc_force_mesh(self, depth, unit)
    return force, torque

size()

Returns magnet dimesions

Returns:

Type Description
size (ndarray

numpy array [width, depth, height]

Source code in src/pymagnet/magnets/_polygon3D.py
115
116
117
118
119
120
121
def size(self):
    """Returns magnet dimesions

    Returns:
        size (ndarray): numpy array [width, depth, height]
    """
    pass

get_total_field_mesh(meshes, x, y, z, r_cut=_np.inf)

Compute the total magnetic field from multiple Mesh magnets in one pass.

Concatenates triangle data from all meshes and evaluates the field at every point (x, y, z) using the points-outer parallel kernel _get_field_parallel_pts_njit. An optional distance cutoff skips triangles whose centroid is farther than r_cut from an evaluation point, which can give a large speedup for sparse or localised geometries.

Parameters:

Name Type Description Default
meshes list

list (or any iterable) of Mesh instances. Pass pm.magnets.Mesh.instances to include all currently registered meshes.

required
x float or ndarray

x co-ordinates of the evaluation points.

required
y float or ndarray

y co-ordinates of the evaluation points.

required
z float or ndarray

z co-ordinates of the evaluation points.

required
r_cut float

distance cutoff in the same length units as the mesh coordinates. Triangles farther than r_cut from a point are skipped. Default: np.inf (no culling — full accuracy).

inf

Returns:

Type Description
Field3

total magnetic field array (attributes .x, .y, .z, .n).

Example::

import pymagnet as pm
import numpy as np

pm.reset()
m1 = pm.magnets.Mesh("left.stl",  Jr=1.0, center=[-30, 0, 0])
m2 = pm.magnets.Mesh("right.stl", Jr=1.0, center=[ 30, 0, 0])

x = np.linspace(-60, 60, 40)
X, Y, Z = np.meshgrid(x, x, x, indexing="ij")

# Single fused pass — equivalent to summing m1.get_field() + m2.get_field()
B = pm.magnets.get_total_field_mesh([m1, m2], X, Y, Z, r_cut=40.0)
Source code in src/pymagnet/magnets/_polygon3D.py
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
def get_total_field_mesh(meshes, x, y, z, r_cut=_np.inf):
    """Compute the total magnetic field from multiple Mesh magnets in one pass.

    Concatenates triangle data from all meshes and evaluates the field at
    every point ``(x, y, z)`` using the points-outer parallel kernel
    ``_get_field_parallel_pts_njit``.  An optional distance cutoff skips
    triangles whose centroid is farther than ``r_cut`` from an evaluation
    point, which can give a large speedup for sparse or localised geometries.

    Args:
        meshes (list): list (or any iterable) of ``Mesh`` instances.  Pass
            ``pm.magnets.Mesh.instances`` to include all currently registered
            meshes.
        x (float or ndarray): x co-ordinates of the evaluation points.
        y (float or ndarray): y co-ordinates of the evaluation points.
        z (float or ndarray): z co-ordinates of the evaluation points.
        r_cut (float): distance cutoff in the same length units as the mesh
            coordinates.  Triangles farther than ``r_cut`` from a point are
            skipped.  Default: ``np.inf`` (no culling — full accuracy).

    Returns:
        Field3: total magnetic field array (attributes ``.x``, ``.y``, ``.z``,
            ``.n``).

    Example::

        import pymagnet as pm
        import numpy as np

        pm.reset()
        m1 = pm.magnets.Mesh("left.stl",  Jr=1.0, center=[-30, 0, 0])
        m2 = pm.magnets.Mesh("right.stl", Jr=1.0, center=[ 30, 0, 0])

        x = np.linspace(-60, 60, 40)
        X, Y, Z = np.meshgrid(x, x, x, indexing="ij")

        # Single fused pass — equivalent to summing m1.get_field() + m2.get_field()
        B = pm.magnets.get_total_field_mesh([m1, m2], X, Y, Z, r_cut=40.0)
    """
    from ..utils._routines3D import _allocate_field_array3

    B = _allocate_field_array3(x, y, z)
    vec_shape = B.x.shape

    x_flat = _np.asarray(x).ravel().astype(_np.float64)
    y_flat = _np.asarray(y).ravel().astype(_np.float64)
    z_flat = _np.asarray(z).ravel().astype(_np.float64)

    rotations, offsets, RA_tris1, RA_tris2, swap_flags, active, centroids, Jnorm = (
        _precompute_all_meshes(meshes)
    )

    Bx, By, Bz = _get_field_parallel_pts_njit(
        rotations,
        offsets,
        RA_tris1,
        RA_tris2,
        swap_flags,
        active,
        centroids,
        Jnorm,
        x_flat,
        y_flat,
        z_flat,
        float(r_cut),
    )

    Bx[~_np.isfinite(Bx)] = 0.0
    By[~_np.isfinite(By)] = 0.0
    Bz[~_np.isfinite(Bz)] = 0.0

    B.x = Bx.reshape(vec_shape)
    B.y = By.reshape(vec_shape)
    B.z = Bz.reshape(vec_shape)
    B.n = _np.linalg.norm([B.x, B.y, B.z], axis=0)
    return B