Skip to content

Utils Module

pymagnets.utils

This module imports the classes and functions in the private modules to create a public API, including:

  • Quaternion()
  • Global Constants
  • Point structures
  • Vector structures

DemagResult dataclass

Result of the demagnetization solver.

Source code in src/pymagnet/utils/_demag.py
20
21
22
23
24
25
26
27
@dataclass
class DemagResult:
    """Result of the demagnetization solver."""

    M_solution: float
    H_int: float
    converged: bool
    solver_used: str  # "brentq" or "fsolve"

Field1

Bases: Point_Array1

1D Field vector This is used to contain one component (Bz as z), and the units ('T', 'mT', etc)

Source code in src/pymagnet/utils/_vector_structs.py
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
class Field1(Point_Array1):
    """1D Field vector
    This is used to contain one component (Bz as z), and the units
    ('T', 'mT', etc)
    """

    def __init__(self, z, unit="T"):
        """Init method

        Args:
            z (ndarray): Magnetic field component
            unit (str, optional): Unit of field. Defaults to "T".

        Raises:
            ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
        """
        super().__init__(z)
        if get_unit_value_tesla(unit) is not None:
            self.unit = unit
        else:
            raise ValueError("Error, not an SI prefix, e.g. T, mT, uT, nT ")

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nBz: {self.z}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nBz: {self.z}]"

    def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
        """Converts field array to a different unit. e.g from 'T' to 'mT'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
        """
        super().change_unit(new_unit, get_unit_value)

__init__(z, unit='T')

Init method

Parameters:

Name Type Description Default
z ndarray

Magnetic field component

required
unit str

Unit of field. Defaults to "T".

'T'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. T, mT, uT, nT

Source code in src/pymagnet/utils/_vector_structs.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
def __init__(self, z, unit="T"):
    """Init method

    Args:
        z (ndarray): Magnetic field component
        unit (str, optional): Unit of field. Defaults to "T".

    Raises:
        ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
    """
    super().__init__(z)
    if get_unit_value_tesla(unit) is not None:
        self.unit = unit
    else:
        raise ValueError("Error, not an SI prefix, e.g. T, mT, uT, nT ")

change_unit(new_unit, get_unit_value=get_unit_value_tesla)

Converts field array to a different unit. e.g from 'T' to 'mT'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_tesla.

get_unit_value_tesla
Source code in src/pymagnet/utils/_vector_structs.py
283
284
285
286
287
288
289
290
def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
    """Converts field array to a different unit. e.g from 'T' to 'mT'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
    """
    super().change_unit(new_unit, get_unit_value)

Field2

Bases: Point_Array2

2D Field vector This is used to contain two components (x, y), and the units ('T', 'mT', etc)

Source code in src/pymagnet/utils/_vector_structs.py
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
class Field2(Point_Array2):
    """2D Field vector
    This is used to contain two components (x, y), and the units
    ('T', 'mT', etc)
    """

    def __init__(self, x, y, unit="T"):
        """Init method

        Args:
            x (ndarray): Magnetic field component Bx
            y (ndarray): Magnetic field component By
            unit (str, optional): Unit of field. Defaults to "T".

        Raises:
            ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
        """
        super().__init__(x, y)
        self.n = _np.zeros_like(x)
        if get_unit_value_tesla(unit) is not None:
            self.unit = unit
        else:
            raise ValueError("Error, not an SI prefix, e.g. T, mT, uT, nT ")

    def calc_norm(self):
        """Calculates the norm of the 2D vector"""
        self.n = _np.linalg.norm([self.x, self.y], axis=0)

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nBx: {self.x}\nBy: {self.y}\nBn: {self.n}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nBx: {self.x}\nBy: {self.y}\nBn: {self.n}]"

    def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
        """Converts field array to a different unit. e.g from 'T' to 'mT'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
        """
        super().change_unit(new_unit, get_unit_value)

__init__(x, y, unit='T')

Init method

Parameters:

Name Type Description Default
x ndarray

Magnetic field component Bx

required
y ndarray

Magnetic field component By

required
unit str

Unit of field. Defaults to "T".

'T'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. T, mT, uT, nT

Source code in src/pymagnet/utils/_vector_structs.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def __init__(self, x, y, unit="T"):
    """Init method

    Args:
        x (ndarray): Magnetic field component Bx
        y (ndarray): Magnetic field component By
        unit (str, optional): Unit of field. Defaults to "T".

    Raises:
        ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
    """
    super().__init__(x, y)
    self.n = _np.zeros_like(x)
    if get_unit_value_tesla(unit) is not None:
        self.unit = unit
    else:
        raise ValueError("Error, not an SI prefix, e.g. T, mT, uT, nT ")

calc_norm()

Calculates the norm of the 2D vector

Source code in src/pymagnet/utils/_vector_structs.py
317
318
319
def calc_norm(self):
    """Calculates the norm of the 2D vector"""
    self.n = _np.linalg.norm([self.x, self.y], axis=0)

change_unit(new_unit, get_unit_value=get_unit_value_tesla)

Converts field array to a different unit. e.g from 'T' to 'mT'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_tesla.

get_unit_value_tesla
Source code in src/pymagnet/utils/_vector_structs.py
327
328
329
330
331
332
333
334
def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
    """Converts field array to a different unit. e.g from 'T' to 'mT'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
    """
    super().change_unit(new_unit, get_unit_value)

Field3

Bases: Point_Array3

3D Field vector This is used to contain three components (x, y), and the units ('T', 'mT', etc)

Source code in src/pymagnet/utils/_vector_structs.py
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
class Field3(Point_Array3):
    """3D Field vector
    This is used to contain three components (x, y), and the units
    ('T', 'mT', etc)
    """

    def __init__(self, x, y, z, unit="T"):
        """Init method

        Args:
            x (ndarray): Magnetic field component Bx
            y (ndarray): Magnetic field component By
            z (ndarray): Magnetic field component Bz
            unit (str, optional): Unit of field. Defaults to "T".

        Raises:
            ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
        """
        super().__init__(x, y, z)
        self.n = _np.zeros_like(x)
        self.unit = unit

    def calc_norm(self):
        """Calculates the norm of the 3D vector"""
        self.n = _np.linalg.norm([self.x, self.y, self.z], axis=0)

    def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
        """Converts field array to a different unit. e.g from 'T' to 'mT'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
        """
        super().change_unit(new_unit, get_unit_value)

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nBx: {self.x}\nBy: {self.y}\nBz: {self.z}\nBn: {self.n}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nBx: {self.x}\nBy: {self.y}\nBz: {self.z}\nBn: {self.n}]"

__init__(x, y, z, unit='T')

Init method

Parameters:

Name Type Description Default
x ndarray

Magnetic field component Bx

required
y ndarray

Magnetic field component By

required
z ndarray

Magnetic field component Bz

required
unit str

Unit of field. Defaults to "T".

'T'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. T, mT, uT, nT

Source code in src/pymagnet/utils/_vector_structs.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
def __init__(self, x, y, z, unit="T"):
    """Init method

    Args:
        x (ndarray): Magnetic field component Bx
        y (ndarray): Magnetic field component By
        z (ndarray): Magnetic field component Bz
        unit (str, optional): Unit of field. Defaults to "T".

    Raises:
        ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
    """
    super().__init__(x, y, z)
    self.n = _np.zeros_like(x)
    self.unit = unit

calc_norm()

Calculates the norm of the 3D vector

Source code in src/pymagnet/utils/_vector_structs.py
359
360
361
def calc_norm(self):
    """Calculates the norm of the 3D vector"""
    self.n = _np.linalg.norm([self.x, self.y, self.z], axis=0)

change_unit(new_unit, get_unit_value=get_unit_value_tesla)

Converts field array to a different unit. e.g from 'T' to 'mT'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_tesla.

get_unit_value_tesla
Source code in src/pymagnet/utils/_vector_structs.py
363
364
365
366
367
368
369
370
def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
    """Converts field array to a different unit. e.g from 'T' to 'mT'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
    """
    super().change_unit(new_unit, get_unit_value)

Jacobian2 dataclass

2D Jacobian tensor of the magnetic field: J_ij = dB_i/dx_j.

Attributes:

Name Type Description
dBx_dx ndarray

Partial derivative of Bx with respect to x.

dBx_dy ndarray

Partial derivative of Bx with respect to y.

dBy_dx ndarray

Partial derivative of By with respect to x.

dBy_dy ndarray

Partial derivative of By with respect to y.

Source code in src/pymagnet/utils/_vector_structs.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
@dataclass
class Jacobian2:
    """2D Jacobian tensor of the magnetic field: J_ij = dB_i/dx_j.

    Attributes:
        dBx_dx: Partial derivative of Bx with respect to x.
        dBx_dy: Partial derivative of Bx with respect to y.
        dBy_dx: Partial derivative of By with respect to x.
        dBy_dy: Partial derivative of By with respect to y.
    """

    dBx_dx: _np.ndarray
    dBx_dy: _np.ndarray
    dBy_dx: _np.ndarray
    dBy_dy: _np.ndarray

Jacobian3 dataclass

3D Jacobian tensor of the magnetic field: J_ij = dB_i/dx_j.

Attributes:

Name Type Description
dBx_dx ndarray

Partial derivative of Bx with respect to x.

dBx_dy ndarray

Partial derivative of Bx with respect to y.

dBx_dz ndarray

Partial derivative of Bx with respect to z.

dBy_dx ndarray

Partial derivative of By with respect to x.

dBy_dy ndarray

Partial derivative of By with respect to y.

dBy_dz ndarray

Partial derivative of By with respect to z.

dBz_dx ndarray

Partial derivative of Bz with respect to x.

dBz_dy ndarray

Partial derivative of Bz with respect to y.

dBz_dz ndarray

Partial derivative of Bz with respect to z.

Source code in src/pymagnet/utils/_vector_structs.py
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
@dataclass
class Jacobian3:
    """3D Jacobian tensor of the magnetic field: J_ij = dB_i/dx_j.

    Attributes:
        dBx_dx: Partial derivative of Bx with respect to x.
        dBx_dy: Partial derivative of Bx with respect to y.
        dBx_dz: Partial derivative of Bx with respect to z.
        dBy_dx: Partial derivative of By with respect to x.
        dBy_dy: Partial derivative of By with respect to y.
        dBy_dz: Partial derivative of By with respect to z.
        dBz_dx: Partial derivative of Bz with respect to x.
        dBz_dy: Partial derivative of Bz with respect to y.
        dBz_dz: Partial derivative of Bz with respect to z.
    """

    dBx_dx: _np.ndarray
    dBx_dy: _np.ndarray
    dBx_dz: _np.ndarray
    dBy_dx: _np.ndarray
    dBy_dy: _np.ndarray
    dBy_dz: _np.ndarray
    dBz_dx: _np.ndarray
    dBz_dy: _np.ndarray
    dBz_dz: _np.ndarray

Point2

2D point class

Note that multiplication of two points is done elementwise, dot product is a separate method.

Source code in src/pymagnet/utils/_point_structs.py
 13
 14
 15
 16
 17
 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
 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
class Point2:
    """2D point class

    Note that multiplication of two points is done elementwise, dot product is
    a separate method.
    """

    def __init__(self, x, y):
        """Init method

        Args:
            x (ndarray): x coordinates
            y (ndarray): y coordinates
        """
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"({self.x}, {self.y})"

    def __str__(self) -> str:
        return f"({self.x}, {self.y})"

    def __add__(self, other):
        return Point2(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Point2(self.x - other.x, self.y - other.y)

    def __mul__(self, other):
        return Point2(self.x * other.x, self.y * other.y)

    def __div__(self, other):
        x = (self.x / other.x) if other.x != 0 else 0
        y = (self.y / other.y) if other.y != 0 else 0
        return Point2(x, y)

    def __lt__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag < other_mag

    def __le__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag <= other_mag

    def __gt__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag > other_mag

    def __ge__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag >= other_mag

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __ne__(self, other):
        return self.x != other.x or self.y != other.y

    def distance_to(self, point):
        """Calculates distance to a point

        Args:
            point (Point2): Target point

        Returns:
            float: distance
        """
        return _np.sqrt(_np.power(point.x - self.x, 2) + _np.power(point.y - self.y, 2))

    def distance_to_origin(self):
        """Calculates distance to a origin

        Returns:
            float: distance
        """
        return self._norm()

    def _norm(self):
        """Norm of Point

        Returns:
            float: norm
        """
        return _np.linalg.norm([self.x, self.y], axis=0)

__init__(x, y)

Init method

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required
Source code in src/pymagnet/utils/_point_structs.py
20
21
22
23
24
25
26
27
28
def __init__(self, x, y):
    """Init method

    Args:
        x (ndarray): x coordinates
        y (ndarray): y coordinates
    """
    self.x = x
    self.y = y

distance_to(point)

Calculates distance to a point

Parameters:

Name Type Description Default
point Point2

Target point

required

Returns:

Type Description
float

distance

Source code in src/pymagnet/utils/_point_structs.py
76
77
78
79
80
81
82
83
84
85
def distance_to(self, point):
    """Calculates distance to a point

    Args:
        point (Point2): Target point

    Returns:
        float: distance
    """
    return _np.sqrt(_np.power(point.x - self.x, 2) + _np.power(point.y - self.y, 2))

distance_to_origin()

Calculates distance to a origin

Returns:

Type Description
float

distance

Source code in src/pymagnet/utils/_point_structs.py
87
88
89
90
91
92
93
def distance_to_origin(self):
    """Calculates distance to a origin

    Returns:
        float: distance
    """
    return self._norm()

Point3

Bases: Point2

3D point class

Note that multiplication of two points is done elementwise, dot product is a separate method.

Source code in src/pymagnet/utils/_point_structs.py
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
class Point3(Point2):
    """3D point class

    Note that multiplication of two points is done elementwise, dot product is
    a separate method.

    """

    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z

    def __repr__(self) -> str:
        return f"({self.x}, {self.y}, {self.z})"

    def __str__(self) -> str:
        return f"({self.x}, {self.y}, {self.z})"

    def __add__(self, other):
        return Point3(self.x + other.x, self.y + other.y, self.z + other.z)

    def __sub__(self, other):
        return Point3(self.x - other.x, self.y - other.y, self.z - other.z)

    def __mul__(self, other):
        return Point3(self.x * other.x, self.y * other.y, self.z * other.z)

    def __div__(self, other):
        x = (self.x / other.x) if other.x != 0 else 0
        y = (self.y / other.y) if other.y != 0 else 0
        z = (self.z / other.z) if other.z != 0 else 0
        return Point3(x, y, z)

    def __lt__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag < other_mag

    def __le__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag <= other_mag

    def __gt__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag > other_mag

    def __ge__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag >= other_mag

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __ne__(self, other):
        return self.x != other.x or self.y != other.y

    def distance_to(self, point):
        return _np.sqrt(
            _np.power(point.x - self.x, 2)
            + _np.power(point.y - self.y, 2)
            + _np.power(point.z - self.z, 2)
        )

    def _norm(self):
        return _np.linalg.norm([self.x, self.y, self.z], axis=0)

    def distance_to_origin(self):
        return self._norm()

Point_Array2

2D point structure This is used to contain two position arrays (x, y), and the units ('mm', 'cm', etc)

Source code in src/pymagnet/utils/_vector_structs.py
 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
class Point_Array2:
    """2D point structure
    This is used to contain two position arrays (x, y), and the units
    ('mm', 'cm', etc)
    """

    def __init__(self, x, y, unit="mm"):
        """Init Method

        Args:
            x (ndarray): x coordinates
            y (ndarray): y coordinates
            unit (str, optional): Unit of length. Defaults to "mm".

        Raises:
            ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
        """
        self.x = _np.asarray(x)
        self.y = _np.asarray(y)
        if get_unit_value_meter(unit) is not None:
            self.unit = unit
        else:
            raise ValueError("Error, not an SI prefix, e.g. km, m, cm, mm ")

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nx: {self.x}\ny: {self.y}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nx: {self.x}\ny: {self.y}]"

    def get_unit(self):
        return self.unit

    def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
        """Converts point array to a different unit. e.g from 'cm' to 'mm'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
        """
        from ..magnets import Circle, Magnet, PolyMagnet, Rectangle, Square

        current_unit_val = get_unit_value(self.get_unit())
        new_unit_val = get_unit_value(new_unit)
        if current_unit_val is None or new_unit_val is None:
            raise ValueError(
                f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
                "unrecognised unit."
            )
        scale_val = current_unit_val / new_unit_val

        self.x *= scale_val
        self.y *= scale_val
        self.unit = new_unit
        for magnet in Magnet.instances:
            magnet.center = scale_val * magnet.center
            if issubclass(magnet.__class__, Rectangle):
                magnet.a = magnet.a * scale_val
                magnet.b = magnet.b * scale_val
                magnet.width = magnet.width * scale_val
                magnet.height = magnet.height * scale_val
            elif issubclass(magnet.__class__, Square):
                magnet.a = magnet.a * scale_val
                magnet.width = magnet.width * scale_val
            elif issubclass(magnet.__class__, Circle):
                magnet.radius = magnet.radius * scale_val
            elif issubclass(magnet.__class__, PolyMagnet):
                magnet.polygon.vertices = (
                    scale_val * _np.array(magnet.polygon.vertices)
                ).tolist()

__init__(x, y, unit='mm')

Init Method

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required
unit str

Unit of length. Defaults to "mm".

'mm'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. km, m, cm, mm

Source code in src/pymagnet/utils/_vector_structs.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def __init__(self, x, y, unit="mm"):
    """Init Method

    Args:
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        unit (str, optional): Unit of length. Defaults to "mm".

    Raises:
        ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
    """
    self.x = _np.asarray(x)
    self.y = _np.asarray(y)
    if get_unit_value_meter(unit) is not None:
        self.unit = unit
    else:
        raise ValueError("Error, not an SI prefix, e.g. km, m, cm, mm ")

change_unit(new_unit, get_unit_value=get_unit_value_meter)

Converts point array to a different unit. e.g from 'cm' to 'mm'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_meter.

get_unit_value_meter
Source code in src/pymagnet/utils/_vector_structs.py
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
def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
    """Converts point array to a different unit. e.g from 'cm' to 'mm'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
    """
    from ..magnets import Circle, Magnet, PolyMagnet, Rectangle, Square

    current_unit_val = get_unit_value(self.get_unit())
    new_unit_val = get_unit_value(new_unit)
    if current_unit_val is None or new_unit_val is None:
        raise ValueError(
            f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
            "unrecognised unit."
        )
    scale_val = current_unit_val / new_unit_val

    self.x *= scale_val
    self.y *= scale_val
    self.unit = new_unit
    for magnet in Magnet.instances:
        magnet.center = scale_val * magnet.center
        if issubclass(magnet.__class__, Rectangle):
            magnet.a = magnet.a * scale_val
            magnet.b = magnet.b * scale_val
            magnet.width = magnet.width * scale_val
            magnet.height = magnet.height * scale_val
        elif issubclass(magnet.__class__, Square):
            magnet.a = magnet.a * scale_val
            magnet.width = magnet.width * scale_val
        elif issubclass(magnet.__class__, Circle):
            magnet.radius = magnet.radius * scale_val
        elif issubclass(magnet.__class__, PolyMagnet):
            magnet.polygon.vertices = (
                scale_val * _np.array(magnet.polygon.vertices)
            ).tolist()

Point_Array3

Bases: Point_Array2

3D point structure This is used to contain three position arrays (x, y, z), and the units ('mm', 'cm', etc)

Source code in src/pymagnet/utils/_vector_structs.py
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
class Point_Array3(Point_Array2):
    """3D point structure
    This is used to contain three position arrays (x, y, z), and the units
    ('mm', 'cm', etc)
    """

    def __init__(self, x, y, z, unit="mm"):
        """Init Method

        Args:
            x (ndarray): x coordinates
            y (ndarray): y coordinates
            z (ndarray): z coordinates
            unit (str, optional): Unit of length. Defaults to "mm".

        Raises:
            ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
        """
        super().__init__(x, y, unit=unit)
        self.z = _np.asarray(z)

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nx: {self.x}\ny: {self.y}\nz: {self.z}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nx: {self.x}\ny: {self.y}\nz: {self.z}]"

    def rotate(self, alpha, beta, gamma):
        """Rotates set of Point_Array3 points by angles alpha, beta, gamma

        Args:
            alpha (float): Angle to rotate about z in degrees
            beta (float): Angle to rotate about y in degrees
            gamma (float): Angle to rotate about x in degrees

        Returns:
            Point_Array3: rotated set of points
        """
        q_fwd = Quaternion.gen_rotation_quaternion(
            _np.deg2rad(alpha), _np.deg2rad(beta), _np.deg2rad(gamma)
        )

        pos_vec = Quaternion._prepare_vector(self.x, self.y, self.z)
        x_rot, y_rot, z_rot = q_fwd * pos_vec
        return Point_Array3(
            x=x_rot.reshape(self.x.shape),
            y=y_rot.reshape(self.x.shape),
            z=z_rot.reshape(self.x.shape),
        )

    def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
        """Converts point array to a different unit. e.g from 'cm' to 'mm'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
        """
        from ..magnets import Cube, Cylinder, Magnet, Mesh, Prism, Sphere

        current_unit_val = get_unit_value(self.get_unit())
        new_unit_val = get_unit_value(new_unit)
        if current_unit_val is None or new_unit_val is None:
            raise ValueError(
                f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
                "unrecognised unit."
            )
        scale_val = current_unit_val / new_unit_val
        self.x *= scale_val
        self.y *= scale_val
        self.z *= scale_val
        self.unit = new_unit

        for magnet in Magnet.instances:
            magnet.center = scale_val * magnet.center
            if issubclass(magnet.__class__, Prism):
                magnet.a = magnet.a * scale_val
                magnet.b = magnet.b * scale_val
                magnet.c = magnet.c * scale_val
                magnet.width = magnet.width * scale_val
                magnet.height = magnet.height * scale_val
                magnet.depth = magnet.depth * scale_val
            elif issubclass(magnet.__class__, Cube):
                magnet.a = magnet.a * scale_val
                magnet.width = magnet.width * scale_val
            elif issubclass(magnet.__class__, Cylinder):
                magnet.radius = magnet.radius * scale_val
                magnet.length = magnet.length * scale_val
            elif issubclass(magnet.__class__, Sphere):
                magnet.radius = magnet.radius * scale_val
            elif issubclass(magnet.__class__, Mesh):
                magnet.mesh_vectors = magnet.mesh_vectors * scale_val

__init__(x, y, z, unit='mm')

Init Method

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required
unit str

Unit of length. Defaults to "mm".

'mm'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. km, m, cm, mm

Source code in src/pymagnet/utils/_vector_structs.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, x, y, z, unit="mm"):
    """Init Method

    Args:
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates
        unit (str, optional): Unit of length. Defaults to "mm".

    Raises:
        ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
    """
    super().__init__(x, y, unit=unit)
    self.z = _np.asarray(z)

change_unit(new_unit, get_unit_value=get_unit_value_meter)

Converts point array to a different unit. e.g from 'cm' to 'mm'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_meter.

get_unit_value_meter
Source code in src/pymagnet/utils/_vector_structs.py
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
def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
    """Converts point array to a different unit. e.g from 'cm' to 'mm'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
    """
    from ..magnets import Cube, Cylinder, Magnet, Mesh, Prism, Sphere

    current_unit_val = get_unit_value(self.get_unit())
    new_unit_val = get_unit_value(new_unit)
    if current_unit_val is None or new_unit_val is None:
        raise ValueError(
            f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
            "unrecognised unit."
        )
    scale_val = current_unit_val / new_unit_val
    self.x *= scale_val
    self.y *= scale_val
    self.z *= scale_val
    self.unit = new_unit

    for magnet in Magnet.instances:
        magnet.center = scale_val * magnet.center
        if issubclass(magnet.__class__, Prism):
            magnet.a = magnet.a * scale_val
            magnet.b = magnet.b * scale_val
            magnet.c = magnet.c * scale_val
            magnet.width = magnet.width * scale_val
            magnet.height = magnet.height * scale_val
            magnet.depth = magnet.depth * scale_val
        elif issubclass(magnet.__class__, Cube):
            magnet.a = magnet.a * scale_val
            magnet.width = magnet.width * scale_val
        elif issubclass(magnet.__class__, Cylinder):
            magnet.radius = magnet.radius * scale_val
            magnet.length = magnet.length * scale_val
        elif issubclass(magnet.__class__, Sphere):
            magnet.radius = magnet.radius * scale_val
        elif issubclass(magnet.__class__, Mesh):
            magnet.mesh_vectors = magnet.mesh_vectors * scale_val

rotate(alpha, beta, gamma)

Rotates set of Point_Array3 points by angles alpha, beta, gamma

Parameters:

Name Type Description Default
alpha float

Angle to rotate about z in degrees

required
beta float

Angle to rotate about y in degrees

required
gamma float

Angle to rotate about x in degrees

required

Returns:

Type Description
Point_Array3

rotated set of points

Source code in src/pymagnet/utils/_vector_structs.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def rotate(self, alpha, beta, gamma):
    """Rotates set of Point_Array3 points by angles alpha, beta, gamma

    Args:
        alpha (float): Angle to rotate about z in degrees
        beta (float): Angle to rotate about y in degrees
        gamma (float): Angle to rotate about x in degrees

    Returns:
        Point_Array3: rotated set of points
    """
    q_fwd = Quaternion.gen_rotation_quaternion(
        _np.deg2rad(alpha), _np.deg2rad(beta), _np.deg2rad(gamma)
    )

    pos_vec = Quaternion._prepare_vector(self.x, self.y, self.z)
    x_rot, y_rot, z_rot = q_fwd * pos_vec
    return Point_Array3(
        x=x_rot.reshape(self.x.shape),
        y=y_rot.reshape(self.x.shape),
        z=z_rot.reshape(self.x.shape),
    )

Quaternion

Quaternion class. overloading of multiplication symbol allows easy quaternion multiplications

Source code in src/pymagnet/utils/_quaternion.py
 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
class Quaternion:
    """Quaternion class.
    overloading of multiplication symbol allows easy quaternion multiplications

    """

    def __init__(
        self,
        w: _np.ndarray | float = 1.0,
        x: _np.ndarray | float = 0.0,
        y: _np.ndarray | float = 0.0,
        z: _np.ndarray | float = 0.0,
    ):
        """Initialse a pure quaternion (1; 0, 0, 0)

        Args:
            w (ndarray, optional): scalar quaternion. Defaults to 1.0.
            x (ndarray, optional): vector component. Defaults to 0.0.
            y (ndarray, optional): vector component. Defaults to 0.0.
            z (ndarray, optional): vector component. Defaults to 0.0.
        """
        self.w = _np.asarray(w)
        self.x = _np.asarray(x)
        self.y = _np.asarray(y)
        self.z = _np.asarray(z)

    def get_conjugate(self) -> Self:
        """Returns quaternion conjugate

        Returns:
            quaternion: quaternion conjugate
        """
        return Quaternion(self.w, -self.x, -self.y, -self.z)  # type: ignore

    @staticmethod
    def gen_rotation_quaternion(alpha_rad=0.0, beta_rad=0.0, gamma_rad=0.0):
        """Generates quaternion for rotation around z, y, x axes

        Args:
            alpha_rad (float): angle to z-axis. Defaults to 0.0.
            beta_rad (float): angle to y-axis. Defaults to 0.0.
            gamma_rad (float): angle to x-axis. Defaults to 0.0.

        Returns:
            Quaternion: rotation quaternion
        """

        rotate_about_x = Quaternion()
        rotate_about_y = Quaternion()
        rotate_about_z = Quaternion()

        forward_rotation = Quaternion()

        if _np.fabs(alpha_rad) > MAG_TOL:
            rotate_about_z = q_angle_from_axis(alpha_rad, (0, 0, 1))

        if _np.fabs(beta_rad) > MAG_TOL:
            rotate_about_y = q_angle_from_axis(beta_rad, (0, 1, 0))

        if _np.fabs(gamma_rad) > MAG_TOL:
            rotate_about_x = q_angle_from_axis(gamma_rad, (1, 0, 0))

        forward_rotation = rotate_about_x * rotate_about_z * rotate_about_y  # type: ignore

        return forward_rotation

    @staticmethod
    def _prepare_vector(x, y, z):
        """Creates 3xN array where each column corresponds to x, y, z vectors
        all of the same length. If some vectors are shorter, their values are repeated
        to ensure and array of (x,y,z) points for quaternion rotation.

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

        Returns:
            ndarray: 3xN numpy ndarray
        """
        # Check input x,y,z are numpy arrays and if not, convert to numpy arrays
        x = _np.asarray(x)
        y = _np.asarray(y)
        z = _np.asarray(z)

        longest_array_length = _np.max([x.size, y.size, z.size])

        # Ensure the unravelled arrays are all of the same length
        x = Quaternion._check_extend_array(x.ravel(), longest_array_length)
        y = Quaternion._check_extend_array(y.ravel(), longest_array_length)
        z = Quaternion._check_extend_array(z.ravel(), longest_array_length)

        return _np.array([x, y, z])

    @staticmethod
    def _check_extend_array(array, max):
        """Extend a 1D vector to length 'max' if it is shorter than 'max'

        Args:
            array (ndarray): numpy array
            max (int): array length to compare to

        Returns:
            ndarray: numpy array of length 'max'
        """

        if max % array.size != 0:
            raise ValueError("Incorrect gridding of x,y,z")
        # If array is smaller than longest array length 'max', extend it by tiling
        if array.size < max:
            extended_array = _np.tile(array, (max // array.size))

            # # If there is a remainder, 'n' append the first n elements of the array
            # # to ensure the final length is 'max'.
            # if max % array.size != 0:
            #     extended_array = _np.append(
            #         extended_array, array[0 : (max % array.size)]
            #     )
            return extended_array
        else:
            return array

    @staticmethod
    def _normalise_axis(vec):
        """Normalise

        Args:
            v (array): [description]

        Returns:
            [type]: [description]
        """
        vec = _np.asarray(vec)
        if _np.fabs(_np.linalg.norm(vec, axis=0)) < FP_CUTOFF:
            raise ValueError("Vec norm should be non-zero")
        return vec / _np.linalg.norm(vec, axis=0)

    @staticmethod
    def vec_norm(x, y, z):
        """Normalises each x,y,z vector

        Args:
            x (ndarray): x array
            y (ndarray): y array
            z (ndarray): z array

        Returns:
            array: 3xN array of normalised vectors
        """
        vec = Quaternion._prepare_vector(x, y, z)
        return _np.linalg.norm(vec, axis=0)

    def __repr__(self):
        str = f"({self.w}; {self.x}, {self.y}, {self.z})"
        return str

    def __mul__(self, b):
        if isinstance(b, Quaternion):
            return self._multiply_with_quaternion(b)
        elif isinstance(b, (list, tuple, _np.ndarray)):
            if len(b) != 3:
                raise ValueError(f"Input vector has invalid length {len(b)}")
            return self._multiply_with_vector(b)
        else:
            raise TypeError(f"Multiplication with unknown type {type(b)}")

    def as_tuple(self):
        """Returns quaternion as tuple of arrays

        Returns:
            tuple: w (array), x (array), y (array), z (array)
        """
        return self.w, self.x, self.y, self.z

    def _multiply_with_quaternion(self, q2):
        """Computes the Hamilton product of two quaternions

        Args:
            q2 (quaternion): quaternion

        Returns:
            quaternion: Hamilton product
        """
        w1, x1, y1, z1 = self.as_tuple()
        w2, x2, y2, z2 = q2.as_tuple()
        w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
        x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
        y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2
        z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
        result = Quaternion(w, x, y, z)
        return result

    def _multiply_with_vector(self, v):
        """Converts vector ``v`` to quaternion ``q2`` and then performs rotation by
        multiplying `q * q2 * q'`


        Args:
            v (vector/array): vector to be rotated

        Returns:
            tuple: multiplied vector x', y', z'
        """
        q2 = Quaternion(_np.zeros_like(v[0]), v[0], v[1], v[2])
        assert q2 is not None
        result = self * q2 * self.get_conjugate()
        return result.x, result.y, result.z

    def get_axisangle(self):
        theta = _np.arccos(self.w) * 2.0
        vec = _np.hstack([self.x, self.y, self.z])
        return theta, self._normalise_axis(vec)

    @staticmethod
    def euler_to_quaternion(alpha, beta, gamma):
        """Converts Euler angles to quaternion

        Args:
            alpha (float): angle to z-axis
            beta (float): angle to y-axis
            gamma (float): angle to x-axis

        Returns:
            Quaternion: Euler angles as a Quaternion
        """

        qw = _np.cos(alpha / 2) * _np.cos(beta / 2) * _np.cos(gamma / 2) + _np.sin(
            alpha / 2
        ) * _np.sin(beta / 2) * _np.sin(gamma / 2)
        qx = _np.sin(alpha / 2) * _np.cos(beta / 2) * _np.cos(gamma / 2) - _np.cos(
            alpha / 2
        ) * _np.sin(beta / 2) * _np.sin(gamma / 2)
        qy = _np.cos(alpha / 2) * _np.sin(beta / 2) * _np.cos(gamma / 2) + _np.sin(
            alpha / 2
        ) * _np.cos(beta / 2) * _np.sin(gamma / 2)
        qz = _np.cos(alpha / 2) * _np.cos(beta / 2) * _np.sin(gamma / 2) - _np.sin(
            alpha / 2
        ) * _np.sin(beta / 2) * _np.cos(gamma / 2)

        return Quaternion(qw, qx, qy, qz)

__init__(w=1.0, x=0.0, y=0.0, z=0.0)

Initialse a pure quaternion (1; 0, 0, 0)

Parameters:

Name Type Description Default
w ndarray

scalar quaternion. Defaults to 1.0.

1.0
x ndarray

vector component. Defaults to 0.0.

0.0
y ndarray

vector component. Defaults to 0.0.

0.0
z ndarray

vector component. Defaults to 0.0.

0.0
Source code in src/pymagnet/utils/_quaternion.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def __init__(
    self,
    w: _np.ndarray | float = 1.0,
    x: _np.ndarray | float = 0.0,
    y: _np.ndarray | float = 0.0,
    z: _np.ndarray | float = 0.0,
):
    """Initialse a pure quaternion (1; 0, 0, 0)

    Args:
        w (ndarray, optional): scalar quaternion. Defaults to 1.0.
        x (ndarray, optional): vector component. Defaults to 0.0.
        y (ndarray, optional): vector component. Defaults to 0.0.
        z (ndarray, optional): vector component. Defaults to 0.0.
    """
    self.w = _np.asarray(w)
    self.x = _np.asarray(x)
    self.y = _np.asarray(y)
    self.z = _np.asarray(z)

as_tuple()

Returns quaternion as tuple of arrays

Returns:

Type Description
tuple

w (array), x (array), y (array), z (array)

Source code in src/pymagnet/utils/_quaternion.py
193
194
195
196
197
198
199
def as_tuple(self):
    """Returns quaternion as tuple of arrays

    Returns:
        tuple: w (array), x (array), y (array), z (array)
    """
    return self.w, self.x, self.y, self.z

euler_to_quaternion(alpha, beta, gamma) staticmethod

Converts Euler angles to quaternion

Parameters:

Name Type Description Default
alpha float

angle to z-axis

required
beta float

angle to y-axis

required
gamma float

angle to x-axis

required

Returns:

Type Description
Quaternion

Euler angles as a Quaternion

Source code in src/pymagnet/utils/_quaternion.py
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
@staticmethod
def euler_to_quaternion(alpha, beta, gamma):
    """Converts Euler angles to quaternion

    Args:
        alpha (float): angle to z-axis
        beta (float): angle to y-axis
        gamma (float): angle to x-axis

    Returns:
        Quaternion: Euler angles as a Quaternion
    """

    qw = _np.cos(alpha / 2) * _np.cos(beta / 2) * _np.cos(gamma / 2) + _np.sin(
        alpha / 2
    ) * _np.sin(beta / 2) * _np.sin(gamma / 2)
    qx = _np.sin(alpha / 2) * _np.cos(beta / 2) * _np.cos(gamma / 2) - _np.cos(
        alpha / 2
    ) * _np.sin(beta / 2) * _np.sin(gamma / 2)
    qy = _np.cos(alpha / 2) * _np.sin(beta / 2) * _np.cos(gamma / 2) + _np.sin(
        alpha / 2
    ) * _np.cos(beta / 2) * _np.sin(gamma / 2)
    qz = _np.cos(alpha / 2) * _np.cos(beta / 2) * _np.sin(gamma / 2) - _np.sin(
        alpha / 2
    ) * _np.sin(beta / 2) * _np.cos(gamma / 2)

    return Quaternion(qw, qx, qy, qz)

gen_rotation_quaternion(alpha_rad=0.0, beta_rad=0.0, gamma_rad=0.0) staticmethod

Generates quaternion for rotation around z, y, x axes

Parameters:

Name Type Description Default
alpha_rad float

angle to z-axis. Defaults to 0.0.

0.0
beta_rad float

angle to y-axis. Defaults to 0.0.

0.0
gamma_rad float

angle to x-axis. Defaults to 0.0.

0.0

Returns:

Type Description
Quaternion

rotation quaternion

Source code in src/pymagnet/utils/_quaternion.py
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
@staticmethod
def gen_rotation_quaternion(alpha_rad=0.0, beta_rad=0.0, gamma_rad=0.0):
    """Generates quaternion for rotation around z, y, x axes

    Args:
        alpha_rad (float): angle to z-axis. Defaults to 0.0.
        beta_rad (float): angle to y-axis. Defaults to 0.0.
        gamma_rad (float): angle to x-axis. Defaults to 0.0.

    Returns:
        Quaternion: rotation quaternion
    """

    rotate_about_x = Quaternion()
    rotate_about_y = Quaternion()
    rotate_about_z = Quaternion()

    forward_rotation = Quaternion()

    if _np.fabs(alpha_rad) > MAG_TOL:
        rotate_about_z = q_angle_from_axis(alpha_rad, (0, 0, 1))

    if _np.fabs(beta_rad) > MAG_TOL:
        rotate_about_y = q_angle_from_axis(beta_rad, (0, 1, 0))

    if _np.fabs(gamma_rad) > MAG_TOL:
        rotate_about_x = q_angle_from_axis(gamma_rad, (1, 0, 0))

    forward_rotation = rotate_about_x * rotate_about_z * rotate_about_y  # type: ignore

    return forward_rotation

get_conjugate()

Returns quaternion conjugate

Returns:

Type Description
quaternion

quaternion conjugate

Source code in src/pymagnet/utils/_quaternion.py
53
54
55
56
57
58
59
def get_conjugate(self) -> Self:
    """Returns quaternion conjugate

    Returns:
        quaternion: quaternion conjugate
    """
    return Quaternion(self.w, -self.x, -self.y, -self.z)  # type: ignore

vec_norm(x, y, z) staticmethod

Normalises each x,y,z vector

Parameters:

Name Type Description Default
x ndarray

x array

required
y ndarray

y array

required
z ndarray

z array

required

Returns:

Type Description
array

3xN array of normalised vectors

Source code in src/pymagnet/utils/_quaternion.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
@staticmethod
def vec_norm(x, y, z):
    """Normalises each x,y,z vector

    Args:
        x (ndarray): x array
        y (ndarray): y array
        z (ndarray): z array

    Returns:
        array: 3xN array of normalised vectors
    """
    vec = Quaternion._prepare_vector(x, y, z)
    return _np.linalg.norm(vec, axis=0)

BdotgradB_2D(B, x, y)

Computes (B . grad)B using the full Jacobian tensor.

Calculates the directional derivative of B along B

[(Bยทโˆ‡)B]_x = Bx * dBx/dx + By * dBx/dy [(Bยทโˆ‡)B]_y = Bx * dBy/dx + By * dBy/dy

This is the force-relevant quantity for magnetic gradient forces on paramagnetic materials.

Parameters:

Name Type Description Default
B Field2

Magnetic field vector with .x, .y components

required
x ndarray

x coordinates (2D grid)

required
y ndarray

y coordinates (2D grid)

required

Returns:

Type Description
Field2

(Bยทโˆ‡)B vector and its norm

Source code in src/pymagnet/utils/_routines2D.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def BdotgradB_2D(B, x, y):
    """Computes (B . grad)B using the full Jacobian tensor.

    Calculates the directional derivative of B along B:
        [(Bยทโˆ‡)B]_x = Bx * dBx/dx + By * dBx/dy
        [(Bยทโˆ‡)B]_y = Bx * dBy/dx + By * dBy/dy

    This is the force-relevant quantity for magnetic gradient forces
    on paramagnetic materials.

    Args:
        B (Field2): Magnetic field vector with .x, .y components
        x (ndarray): x coordinates (2D grid)
        y (ndarray): y coordinates (2D grid)

    Returns:
        Field2: (Bยทโˆ‡)B vector and its norm
    """
    J = jacobian_B_2D(B, x, y)
    F = Field2(_np.zeros_like(B.x), _np.zeros_like(B.y))
    F.x = B.x * J.dBx_dx + B.y * J.dBx_dy
    F.y = B.x * J.dBy_dx + B.y * J.dBy_dy
    F.calc_norm()
    return F

BdotgradB_3D(B, x, y, z)

Computes (B . grad)B using the full Jacobian tensor.

Calculates the directional derivative of B along B

[(Bยทโˆ‡)B]_x = Bx * dBx/dx + By * dBx/dy + Bz * dBx/dz [(Bยทโˆ‡)B]_y = Bx * dBy/dx + By * dBy/dy + Bz * dBy/dz [(Bยทโˆ‡)B]_z = Bx * dBz/dx + By * dBz/dy + Bz * dBz/dz

Supports both full 3D grids and 2D slices.

Parameters:

Name Type Description Default
B Field3

Magnetic field vector with .x, .y, .z components

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
Field3

(Bยทโˆ‡)B vector and its norm

Source code in src/pymagnet/utils/_routines3D.py
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
def BdotgradB_3D(B, x, y, z):
    """Computes (B . grad)B using the full Jacobian tensor.

    Calculates the directional derivative of B along B:
        [(Bยทโˆ‡)B]_x = Bx * dBx/dx + By * dBx/dy + Bz * dBx/dz
        [(Bยทโˆ‡)B]_y = Bx * dBy/dx + By * dBy/dy + Bz * dBy/dz
        [(Bยทโˆ‡)B]_z = Bx * dBz/dx + By * dBz/dy + Bz * dBz/dz

    Supports both full 3D grids and 2D slices.

    Args:
        B (Field3): Magnetic field vector with .x, .y, .z components
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates

    Returns:
        Field3: (Bยทโˆ‡)B vector and its norm
    """
    J = jacobian_B_3D(B, x, y, z)
    F = Field3(_np.zeros_like(B.x), _np.zeros_like(B.y), _np.zeros_like(B.z))
    F.x = B.x * J.dBx_dx + B.y * J.dBx_dy + B.z * J.dBx_dz
    F.y = B.x * J.dBy_dx + B.y * J.dBy_dy + B.z * J.dBy_dz
    F.z = B.x * J.dBz_dx + B.y * J.dBz_dy + B.z * J.dBz_dz
    F.calc_norm()
    return F

FgradB_2D(B, x, y, chi_m, c)

Calculates the magnetic field gradient force for a 2D field.

Computes F = (chi_m / mu_0) * c * |B| * grad(|B|). This is a scalar approximation of the gradient force.

Parameters:

Name Type Description Default
B Field2

Magnetic field vector (must have .n for magnitude)

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
chi_m float

Magnetic susceptibility

required
c float

Material constant

required

Returns:

Type Description
Field2

Magnetic field gradient force vector

Source code in src/pymagnet/utils/_routines2D.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def FgradB_2D(B, x, y, chi_m, c):
    """Calculates the magnetic field gradient force for a 2D field.

    Computes F = (chi_m / mu_0) * c * |B| * grad(|B|).
    This is a scalar approximation of the gradient force.

    Args:
        B (Field2): Magnetic field vector (must have .n for magnitude)
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        chi_m (float): Magnetic susceptibility
        c (float): Material constant

    Returns:
        Field2: Magnetic field gradient force vector
    """
    dB = gradB_2D(B.n, x, y)
    scale = (1 / MU0) * chi_m * c
    FB = Field2(_np.zeros_like(B.n), _np.zeros_like(B.n))
    FB.x = scale * dB.x * B.n
    FB.y = scale * dB.y * B.n
    FB.n = scale * dB.n * B.n
    return FB

FgradB_3D(B, x, y, z, chi_m, c)

Calculates the magnetic field gradient force for a 3D field.

Computes F = (chi_m / mu_0) * c * |B| * grad(|B|). This is a scalar approximation of the gradient force.

Parameters:

Name Type Description Default
B Field3

Magnetic field vector (must have .n for magnitude)

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required
chi_m float

Magnetic susceptibility

required
c float

Material constant

required

Returns:

Type Description
Field3

Magnetic field gradient force vector

Source code in src/pymagnet/utils/_routines3D.py
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
def FgradB_3D(B, x, y, z, chi_m, c):
    """Calculates the magnetic field gradient force for a 3D field.

    Computes F = (chi_m / mu_0) * c * |B| * grad(|B|).
    This is a scalar approximation of the gradient force.

    Args:
        B (Field3): Magnetic field vector (must have .n for magnitude)
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates
        chi_m (float): Magnetic susceptibility
        c (float): Material constant

    Returns:
        Field3: Magnetic field gradient force vector
    """
    dB = gradB_3D(B.n, x, y, z)
    scale = (1 / MU0) * chi_m * c
    FB = Field3(_np.zeros_like(B.n), _np.zeros_like(B.n), _np.zeros_like(B.n))
    FB.x = scale * dB.x * B.n
    FB.y = scale * dB.y * B.n
    FB.z = scale * dB.z * B.n
    FB.n = scale * dB.n * B.n
    return FB

altitude(a, b, c)

Gets altitude to side a of a triangle using Heron's formula.

Parameters:

Name Type Description Default
a float

base side (altitude is perpendicular to this)

required
b float

second side

required
c float

third side

required

Returns:

Type Description
float

altitude to side a

Raises:

Type Description
ValueError

If triangle is degenerate (sides violate triangle inequality)

Source code in src/pymagnet/utils/_trigonometry3D.py
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
def altitude(a, b, c):
    """Gets altitude to side `a` of a triangle using Heron's formula.

    Args:
        a (float): base side (altitude is perpendicular to this)
        b (float): second side
        c (float): third side

    Returns:
        float: altitude to side `a`

    Raises:
        ValueError: If triangle is degenerate (sides violate triangle inequality)
    """
    if a < 1e-10:
        raise ValueError("Degenerate triangle: base side has zero length")

    s = (a + b + c) / 2
    radicand = s * (s - a) * (s - b) * (s - c)

    if radicand < 0:
        raise ValueError(
            f"Degenerate triangle: sides ({a:.6f}, {b:.6f}, {c:.6f}) "
            "violate triangle inequality"
        )

    return 2 * _np.sqrt(radicand) / a

build_MH_interpolator(H_data, M_data)

Build a monotonicity-preserving interpolator from an MH curve.

Parameters:

Name Type Description Default
H_data ndarray

Applied field values, strictly monotonically increasing.

required
M_data ndarray

Magnetization values, monotonically non-decreasing.

required

Returns:

Type Description
PchipInterpolator

PchipInterpolator mapping H -> M.

Source code in src/pymagnet/utils/_demag.py
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
def build_MH_interpolator(
    H_data: _np.ndarray, M_data: _np.ndarray
) -> PchipInterpolator:
    """Build a monotonicity-preserving interpolator from an MH curve.

    Args:
        H_data: Applied field values, strictly monotonically increasing.
        M_data: Magnetization values, monotonically non-decreasing.

    Returns:
        PchipInterpolator mapping H -> M.
    """
    H_data = _np.asarray(H_data, dtype=float)
    M_data = _np.asarray(M_data, dtype=float)

    if H_data.shape != M_data.shape:
        raise ValueError(
            f"H_data and M_data must have the same shape, "
            f"got {H_data.shape} and {M_data.shape}"
        )

    if H_data.ndim != 1:
        raise ValueError(f"Expected 1D arrays, got ndim={H_data.ndim}")

    dH = _np.diff(H_data)
    if _np.any(dH <= 0):
        raise ValueError("H_data must be strictly monotonically increasing")

    dM = _np.diff(M_data)
    if _np.any(dM < 0):
        raise ValueError("M_data must be monotonically non-decreasing")

    return PchipInterpolator(H_data, M_data)

get_field_2D(Point_Array2)

Calculates magnetic field at an array of points due to every instantated Magnet2D magnet.

Parameters:

Name Type Description Default
Point_Array2 Point_Array2

array of x,y points and associated unit, defaults to 'mm'

required

Returns:

Type Description
Field2

array of Bx,By,|B| values and associated unit (defaults to 'T')

Source code in src/pymagnet/utils/_routines2D.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def get_field_2D(Point_Array2):
    """Calculates magnetic field at an array of points due to every instantated
    `Magnet2D` magnet.

    Args:
        Point_Array2 (Point_Array2): array of x,y points and associated unit,
            defaults to 'mm'

    Returns:
        Field2: array of Bx,By,|B| values and associated unit (defaults to 'T')
    """
    from ..magnets import Magnet2D

    # Empty data structure
    B = _allocate_field_array2(Point_Array2.x, Point_Array2.y)

    for magnet in Magnet2D.instances:
        Bx, By = magnet.get_field(Point_Array2.x, Point_Array2.y)
        B.x += Bx
        B.y += By

    B.calc_norm()
    return B

get_field_3D(points)

Calculates magnetic field at an array of points due to every instantated Magnet3D magnet.

Parameters:

Name Type Description Default
points Point_Array3

array of x,y,z points and associated unit, defaults to 'mm'

required

Returns:

Type Description
Field3

array of Bx,By,Bz,|B| values and associated unit (defaults to 'T')

Source code in src/pymagnet/utils/_routines3D.py
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
def get_field_3D(points):
    """Calculates magnetic field at an array of points due to every instantated
    `Magnet3D` magnet.

    Args:
        points (Point_Array3): array of x,y,z points and associated unit,
            defaults to 'mm'

    Returns:
        Field3: array of Bx,By,Bz,|B| values and associated unit (defaults to 'T')
    """
    from ..magnets import Magnet3D

    B = _allocate_field_array3(points.x, points.y, points.z)

    # Track which points are inside any magnet (for masking after summation)
    mask_magnet = _np.zeros(B.x.shape, dtype=bool)
    needs_masking = False

    for magnet in Magnet3D.instances:
        Bx, By, Bz = magnet.get_field(points.x, points.y, points.z)
        Bx = Bx.reshape(B.x.shape)
        By = By.reshape(B.y.shape)
        Bz = Bz.reshape(B.z.shape)

        # Replace NaN with zero before accumulation to prevent NaN
        # poisoning the sum (NaN + valid = NaN). NaN can arise from
        # mask_magnet flagging points inside a magnet, or from
        # numerical singularities at magnet edges.
        nan_mask = _np.isnan(Bx) | _np.isnan(By) | _np.isnan(Bz)
        if _np.any(nan_mask):
            Bx = _np.where(nan_mask, 0.0, Bx)
            By = _np.where(nan_mask, 0.0, By)
            Bz = _np.where(nan_mask, 0.0, Bz)

            if magnet._mask_magnet:
                mask_magnet |= nan_mask
                needs_masking = True

        B.x += Bx
        B.y += By
        B.z += Bz

    # Re-apply NaN mask for points inside any magnet
    if needs_masking:
        B.x[mask_magnet] = _np.nan
        B.y[mask_magnet] = _np.nan
        B.z[mask_magnet] = _np.nan

    B.calc_norm()
    return B

gradB_2D(B, x, y)

Calculates the spatial gradient of the magnetic field magnitude.

Computes grad(|B|) using finite differences. Uses numba-accelerated kernels for grids >= 10k points, np.gradient for smaller grids.

Parameters:

Name Type Description Default
B ndarray

Magnetic field magnitude |B| (2D array)

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required

Returns:

Type Description
Field2

Gradient vector (d|B|/dx, d|B|/dy) and its norm

Source code in src/pymagnet/utils/_routines2D.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def gradB_2D(B, x, y):
    """Calculates the spatial gradient of the magnetic field magnitude.

    Computes grad(|B|) using finite differences. Uses numba-accelerated
    kernels for grids >= 10k points, np.gradient for smaller grids.

    Args:
        B (ndarray): Magnetic field magnitude |B| (2D array)
        x (ndarray): x coordinates
        y (ndarray): y coordinates

    Returns:
        Field2: Gradient vector (d|B|/dx, d|B|/dy) and its norm
    """
    dx, dy = _grid_spacing_2d(x, y)
    dB = Field2(_np.zeros_like(B), _np.zeros_like(B))
    if B.size >= _NUMBA_THRESHOLD:
        dB.x, dB.y = _gradient_2d(_np.ascontiguousarray(B), dx, dy)
    else:
        dB.x, dB.y = _np.gradient(B, dx, dy)
    dB.calc_norm()
    return dB

gradB_3D(B, x, y, z)

Calculates the spatial gradient of the magnetic field magnitude.

Computes grad(|B|) using finite differences. Uses numba-accelerated kernels for grids >= 10k points, np.gradient for smaller grids. Supports both full 3D grids (from grid3D) and 2D slices (from slice3D).

Parameters:

Name Type Description Default
B ndarray

Magnetic field magnitude |B|

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
Field3

Gradient vector (d|B|/dx, d|B|/dy, d|B|/dz) and its norm

Source code in src/pymagnet/utils/_routines3D.py
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
def gradB_3D(B, x, y, z):
    """Calculates the spatial gradient of the magnetic field magnitude.

    Computes grad(|B|) using finite differences. Uses numba-accelerated
    kernels for grids >= 10k points, np.gradient for smaller grids.
    Supports both full 3D grids (from grid3D) and 2D slices (from slice3D).

    Args:
        B (ndarray): Magnetic field magnitude |B|
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates

    Returns:
        Field3: Gradient vector (d|B|/dx, d|B|/dy, d|B|/dz) and its norm
    """
    B_arr = _np.ascontiguousarray(B)
    use_numba = B_arr.size >= _NUMBA_THRESHOLD

    if B_arr.ndim == 3:
        dx, dy, dz = _grid_spacing_3d(x, y, z)
        if use_numba:
            dBdx, dBdy, dBdz = _gradient_3d(B_arr, dx, dy, dz)
        else:
            dBdx, dBdy, dBdz = _np.gradient(B_arr, dx, dy, dz)
    elif B_arr.ndim == 2:
        varying, _constant, coords = _detect_slice_axes(x, y, z)
        c1, c2 = coords[varying[0]], coords[varying[1]]
        N1, N2 = B_arr.shape
        d1 = (c1.max() - c1.min()) / N1
        d2 = (c2.max() - c2.min()) / N2
        if use_numba:
            grad1, grad2 = _gradient_2d(B_arr, d1, d2)
        else:
            grad1, grad2 = _np.gradient(B_arr, d1, d2)

        components = {
            "x": _np.zeros_like(B_arr),
            "y": _np.zeros_like(B_arr),
            "z": _np.zeros_like(B_arr),
        }
        components[varying[0]] = grad1
        components[varying[1]] = grad2
        dBdx, dBdy, dBdz = components["x"], components["y"], components["z"]
    else:
        raise ValueError(f"B must be 2D (slice) or 3D (grid), got ndim={B_arr.ndim}")

    dB = Field3(dBdx, dBdy, dBdz)
    dB.calc_norm()
    return dB

grid2D(xmax, ymax, **kwargs)

Generates grid of x and y points

Parameters:

Name Type Description Default
xmax float

maximum x value

required
ymax float

maximum y value

required
Kwargs

num_points (int): Number of points in each direction. Defaults to 100 xmin (float): minimum x value. Defaults to -xmax ymin (float): minimum y value. Defaults to -ymax unit (str): unit length. Defaults to 'mm'

Returns:

Type Description
Point_Array2

array of x and y values of shape (num_points, num_points) and associated unit

Source code in src/pymagnet/utils/_routines2D.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def grid2D(xmax, ymax, **kwargs):
    """Generates grid of x and y points

    Args:
        xmax (float): maximum x value
        ymax (float): maximum y value

    Kwargs:
        num_points (int): Number of points in each direction. Defaults to 100
        xmin (float): minimum x value. Defaults to -xmax
        ymin (float): minimum y value. Defaults to -ymax
        unit (str): unit length. Defaults to 'mm'

    Returns:
        Point_Array2: array of x and y values of shape (num_points, num_points)
            and associated unit
    """
    num_points = kwargs.pop("num_points", 100)
    xmin = kwargs.pop("xmin", -1 * xmax)
    ymin = kwargs.pop("ymin", -1 * ymax)
    unit = kwargs.pop("unit", "mm")
    NPJ = num_points * 1j
    x, y = _np.mgrid[xmin:xmax:NPJ, ymin:ymax:NPJ]
    return Point_Array2(x, y, unit=unit)

grid3D(xmax, ymax, zmax, **kwargs)

Generates grid of x, y, z points

Parameters:

Name Type Description Default
xmax float

maximum x value

required
ymax float

maximum y value

required
zmax float

maximum y value

required
Kwargs

num_points (int): Number of points in each direction. Defaults to 100 xmin (float): minimum x value. Defaults to -xmax ymin (float): minimum y value. Defaults to -ymax zmin (float): minimum y value. Defaults to -zmax unit (string): unit length. Defaults to 'mm'

Returns:

Type Description
Point_Array2

array of x and y values of shape (num_points, num_points) and associated unit

Source code in src/pymagnet/utils/_routines3D.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
76
77
78
79
def grid3D(xmax, ymax, zmax, **kwargs):
    """Generates grid of x, y, z points

    Args:
        xmax (float): maximum x value
        ymax (float): maximum y value
        zmax (float): maximum y value

    Kwargs:
        num_points (int): Number of points in each direction. Defaults to 100
        xmin (float): minimum x value. Defaults to -xmax
        ymin (float): minimum y value. Defaults to -ymax
        zmin (float): minimum y value. Defaults to -zmax
        unit (string): unit length. Defaults to 'mm'

    Returns:
        Point_Array2: array of x and y values of shape (num_points, num_points) and associated unit
    """
    num_points = kwargs.pop("num_points", None)

    xmin = kwargs.pop("xmin", -1 * xmax)
    ymin = kwargs.pop("ymin", -1 * ymax)
    zmin = kwargs.pop("zmin", -1 * zmax)
    unit = kwargs.pop("unit", "mm")
    # NPJ = num_points * 1j

    if num_points is None:
        num_points_x = kwargs.pop("num_points_x", 100)
        num_points_y = kwargs.pop("num_points_y", 100)
        num_points_z = kwargs.pop("num_points_z", 100)
    else:
        num_points_x = num_points
        num_points_y = num_points
        num_points_z = num_points

    x, y, z = _np.mgrid[
        xmin : xmax : num_points_x * 1j,
        ymin : ymax : num_points_y * 1j,
        zmin : zmax : num_points_z * 1j,
    ]

    return Point_Array3(x, y, z, unit=unit)

jacobian_B_2D(B, x, y)

Calculates the Jacobian of the 2D magnetic field vector.

Computes J_ij = dB_i/dx_j, the full tensor gradient of the vector field.

Parameters:

Name Type Description Default
B Field2

Magnetic field vector with .x, .y components

required
x ndarray

x coordinates (2D grid)

required
y ndarray

y coordinates (2D grid)

required

Returns:

Type Description
Jacobian2

Dataclass with components dBx_dx, dBx_dy, dBy_dx, dBy_dy

Source code in src/pymagnet/utils/_routines2D.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def jacobian_B_2D(B, x, y):
    """Calculates the Jacobian of the 2D magnetic field vector.

    Computes J_ij = dB_i/dx_j, the full tensor gradient of the vector field.

    Args:
        B (Field2): Magnetic field vector with .x, .y components
        x (ndarray): x coordinates (2D grid)
        y (ndarray): y coordinates (2D grid)

    Returns:
        Jacobian2: Dataclass with components dBx_dx, dBx_dy, dBy_dx, dBy_dy
    """
    dx, dy = _grid_spacing_2d(x, y)
    if B.x.size >= _NUMBA_THRESHOLD:
        dBx_dx, dBx_dy = _gradient_2d(_np.ascontiguousarray(B.x), dx, dy)
        dBy_dx, dBy_dy = _gradient_2d(_np.ascontiguousarray(B.y), dx, dy)
    else:
        dBx_dx, dBx_dy = _np.gradient(B.x, dx, dy)
        dBy_dx, dBy_dy = _np.gradient(B.y, dx, dy)
    return Jacobian2(dBx_dx=dBx_dx, dBx_dy=dBx_dy, dBy_dx=dBy_dx, dBy_dy=dBy_dy)

jacobian_B_3D(B, x, y, z)

Calculates the Jacobian of the 3D magnetic field vector.

Computes J_ij = dB_i/dx_j, the full tensor gradient of the vector field. Supports both full 3D grids (from grid3D) and 2D slices (from slice3D).

Parameters:

Name Type Description Default
B Field3

Magnetic field vector with .x, .y, .z components

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
Jacobian3

Dataclass with all 9 partial derivative components

Source code in src/pymagnet/utils/_routines3D.py
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
def jacobian_B_3D(B, x, y, z):
    """Calculates the Jacobian of the 3D magnetic field vector.

    Computes J_ij = dB_i/dx_j, the full tensor gradient of the vector field.
    Supports both full 3D grids (from grid3D) and 2D slices (from slice3D).

    Args:
        B (Field3): Magnetic field vector with .x, .y, .z components
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates

    Returns:
        Jacobian3: Dataclass with all 9 partial derivative components
    """
    Bx = _np.ascontiguousarray(B.x)
    By = _np.ascontiguousarray(B.y)
    Bz = _np.ascontiguousarray(B.z)
    use_numba = Bx.size >= _NUMBA_THRESHOLD

    if Bx.ndim == 3:
        dx, dy, dz = _grid_spacing_3d(x, y, z)
        if use_numba:
            dBx_dx, dBx_dy, dBx_dz = _gradient_3d(Bx, dx, dy, dz)
            dBy_dx, dBy_dy, dBy_dz = _gradient_3d(By, dx, dy, dz)
            dBz_dx, dBz_dy, dBz_dz = _gradient_3d(Bz, dx, dy, dz)
        else:
            dBx_dx, dBx_dy, dBx_dz = _np.gradient(Bx, dx, dy, dz)
            dBy_dx, dBy_dy, dBy_dz = _np.gradient(By, dx, dy, dz)
            dBz_dx, dBz_dy, dBz_dz = _np.gradient(Bz, dx, dy, dz)
    elif Bx.ndim == 2:
        varying, _constant, coords = _detect_slice_axes(x, y, z)
        c1, c2 = coords[varying[0]], coords[varying[1]]
        N1, N2 = Bx.shape
        d1 = (c1.max() - c1.min()) / N1
        d2 = (c2.max() - c2.min()) / N2

        zeros = _np.zeros_like(Bx)
        grad_fn = (
            _gradient_2d if use_numba else lambda F, d1, d2: _np.gradient(F, d1, d2)
        )
        # Compute gradients for each B component on the 2D slice
        result = {}
        for comp_name, comp_arr in [("Bx", Bx), ("By", By), ("Bz", Bz)]:
            g1, g2 = grad_fn(comp_arr, d1, d2)
            grads = {"x": zeros.copy(), "y": zeros.copy(), "z": zeros.copy()}
            grads[varying[0]] = g1
            grads[varying[1]] = g2
            result[comp_name] = grads

        dBx_dx, dBx_dy, dBx_dz = result["Bx"]["x"], result["Bx"]["y"], result["Bx"]["z"]
        dBy_dx, dBy_dy, dBy_dz = result["By"]["x"], result["By"]["y"], result["By"]["z"]
        dBz_dx, dBz_dy, dBz_dz = result["Bz"]["x"], result["Bz"]["y"], result["Bz"]["z"]
    else:
        raise ValueError(
            f"B components must be 2D (slice) or 3D (grid), got ndim={Bx.ndim}"
        )

    return Jacobian3(
        dBx_dx=dBx_dx,
        dBx_dy=dBx_dy,
        dBx_dz=dBx_dz,
        dBy_dx=dBy_dx,
        dBy_dy=dBy_dy,
        dBy_dz=dBy_dz,
        dBz_dx=dBz_dx,
        dBz_dy=dBz_dy,
        dBz_dz=dBz_dz,
    )

line3D(start, end, num_points=100, **kwargs)

Generates a line of points

Parameters:

Name Type Description Default
start tuple

Starting point (x1,y1,z1)

required
end tuple

End point (x2,y2,z2)

required
num_points int

number of points to generate. Defaults to 100

100

Other Parameters:

Name Type Description
unit str

length scale units. Defaults to "mm".

Returns:

Type Description
Point_Array3

array of x, y, and z values of shape (num_points) and associated unit

Source code in src/pymagnet/utils/_routines3D.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def line3D(start, end, num_points=100, **kwargs):
    """Generates a line of points

    Args:
        start (tuple): Starting point (x1,y1,z1)
        end (tuple): End point (x2,y2,z2)
        num_points (int): number of points to generate. Defaults to 100

    Other Parameters:
        unit (str): length scale units. Defaults to "mm".

    Returns:
        Point_Array3: array of x, y, and z values of shape (num_points) and associated unit
    """

    unit = kwargs.pop("unit", "mm")

    return Point_Array3(
        x=_np.linspace(start[0], end[0], num_points),
        y=_np.linspace(start[1], end[1], num_points),
        z=_np.linspace(start[2], end[2], num_points),
        unit=unit,
    )

norm_plane(vec)

Calculates the normal to a triangular plane.

Parameters:

Name Type Description Default
vec ndarray

(3,3) array of triangle vertices

required

Returns:

Type Description
ndarray

unit normal vector (3,)

Raises:

Type Description
ValueError

If triangle is degenerate (collinear vertices)

Source code in src/pymagnet/utils/_trigonometry3D.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def norm_plane(vec):
    """Calculates the normal to a triangular plane.

    Args:
        vec (ndarray): (3,3) array of triangle vertices

    Returns:
        ndarray: unit normal vector (3,)

    Raises:
        ValueError: If triangle is degenerate (collinear vertices)
    """
    norm = _np.cross(vec[1] - vec[0], vec[2] - vec[0])
    length = _np.linalg.norm(norm)

    if length < 1e-10:
        raise ValueError("Degenerate triangle: vertices are collinear")

    return norm / length

rotate_points(points, rotation_quaternion)

Rotates a set of points

Parameters:

Name Type Description Default
points [type]

[description]

required
rotation_quaternion [type]

[description]

required

Returns:

Type Description
[type]

[description]

Source code in src/pymagnet/utils/_trigonometry3D.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def rotate_points(points, rotation_quaternion):
    """Rotates a set of points

    Args:
        points ([type]): [description]
        rotation_quaternion ([type]): [description]

    Returns:
        [type]: [description]
    """
    x_rot, y_rot, z_rot = rotation_quaternion * points.T
    rotate_points = _np.vstack([x_rot, y_rot, z_rot]).T

    return rotate_points

rotate_points_2D(x, y, alpha)

Counter-clockwise rotation of points x,y

Rotates 2D coordinates using a rotation matrix

Parameters:

Name Type Description Default
x ndarray

array of x coordinates

required
y ndarray

array of x coordinates

required
alpha float

rotation angle w.r.t. x-axis

required

Returns:

Type Description
tuple

(x', y') rotated array of points

Source code in src/pymagnet/utils/_routines2D.py
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
def rotate_points_2D(x, y, alpha):
    """Counter-clockwise rotation of points x,y

    Rotates 2D coordinates using a rotation matrix

    Args:
        x (ndarray): array of x coordinates
        y (ndarray): array of x coordinates
        alpha (float): rotation angle w.r.t. x-axis

    Returns:
        tuple: (x', y') rotated array of points
    """
    x = _np.atleast_1d(x)
    y = _np.atleast_1d(y)
    if len(x) != len(y):
        raise ValueError("Must have same number of points in x and y")

    rot_matrix = _np.array(
        [[_np.cos(alpha), -_np.sin(alpha)], [_np.sin(alpha), _np.cos(alpha)]]
    )
    stacked_points = _np.column_stack((_np.ravel(x), _np.ravel(y)))
    rotated_points = _np.dot(rot_matrix, stacked_points.T)
    x_rotated = rotated_points[0, :]
    y_rotated = rotated_points[1, :]

    return _np.reshape(x_rotated, x.shape), _np.reshape(y_rotated, y.shape)

signed_area(triangle)

Calculates signed area of a triangle. Area area < 0 for clockwise ordering. Assumes the triangle is in the xz plane (i.e. with the normal parallel to y).

Parameters:

Name Type Description Default
triangle ndarray

3x3 array of vertices

required

Returns:

Type Description
float

signed area

Source code in src/pymagnet/utils/_trigonometry3D.py
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
@jit
def signed_area(triangle):
    """Calculates signed area of a triangle. Area area < 0 for clockwise ordering.
    Assumes the triangle is in the xz plane (i.e. with the normal parallel to y).

    Args:
        triangle (ndarray): 3x3 array of vertices

    Returns:
        float: signed area
    """

    j = 1
    NP = 3
    area = 0.0

    for i in range(NP):
        j = j % NP

        area += (triangle[j][0] - triangle[i][0]) * (triangle[j][2] + triangle[i][2])
        j += 1

    # check winding order of polygon, area < 0 for clockwise ordering of points
    area /= 2.0

    return area

slice3D(plane='xy', max1=1.0, max2=1.0, slice_value=0.0, unit='mm', **kwargs)

Generates a planar slice of values

Parameters:

Name Type Description Default
plane str

plane. Defaults to "xy".

'xy'
max1 float

maximum along axis 1. Defaults to 1.0.

1.0
max2 float

maximum along axis 2. Defaults to 1.0.

1.0
slice_value float

constant value for third axis. Defaults to 0.0.

0.0
unit str

length scale units. Defaults to "mm".

'mm'
Kwargs

num_points (int): Number of points in each direction. Defaults to 100 min1 (float): minimum along axis 1. Defaults to -min1 min2 (float): minimum along axis 2. Defaults to -min2

Raises:

Type Description
Exception

plane type, must be one of 'xy', 'xz, 'yz', or 'custom'

Returns:

Type Description
Point_Array3

array of x, y, and z values of shape (num_points, num_points) and associated unit

Source code in src/pymagnet/utils/_routines3D.py
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
def slice3D(plane="xy", max1=1.0, max2=1.0, slice_value=0.0, unit="mm", **kwargs):
    """Generates a planar slice of values

    Args:
        plane (str, optional): plane. Defaults to "xy".
        max1 (float, optional): maximum along axis 1. Defaults to 1.0.
        max2 (float, optional): maximum along axis 2. Defaults to 1.0.
        slice_value (float, optional): constant value for third axis. Defaults to 0.0.
        unit (str, optional): length scale units. Defaults to "mm".

    Kwargs:
        num_points (int): Number of points in each direction. Defaults to 100
        min1 (float): minimum along axis 1. Defaults to -min1
        min2 (float): minimum along axis 2. Defaults to -min2

    Raises:
        Exception: plane type, must be one of 'xy', 'xz, 'yz', or 'custom'

    Returns:
        Point_Array3: array of x, y, and z values of shape (num_points, num_points) and associated unit
    """
    num_points = kwargs.pop("num_points", 100)
    min1 = kwargs.pop("min1", -1 * max1)
    min2 = kwargs.pop("min2", -1 * max2)
    NPj = num_points * 1j

    if plane.lower() == "xy":
        x, y = _np.mgrid[min1:max1:NPj, min2:max2:NPj]
        z = _np.asarray([slice_value])
        z = _np.tile(z, x.shape)

    elif plane.lower() == "xz":
        x, z = _np.mgrid[min1:max1:NPj, min2:max2:NPj]
        y = _np.asarray([slice_value])
        y = _np.tile(y, x.shape)

    elif plane.lower() == "yz":
        y, z = _np.mgrid[min1:max1:NPj, min2:max2:NPj]
        x = _np.asarray([slice_value])
        x = _np.tile(x, y.shape)

    elif plane.lower() == "custom":
        x = kwargs.pop("custom_x", _np.array([0.0]))
        y = kwargs.pop("custom_y", _np.array([0.0]))
        z = kwargs.pop("custom_z", _np.array([0.0]))

    else:
        raise ValueError("plane must be one of 'xy', 'xz, 'yz', or 'custom'")

    return Point_Array3(x, y, z, unit=unit)

solve_demag_tanh(H_ext, Ms, chi, N=0.5)

Solve demagnetization for the tanh model (single scalar H_ext).

Parameters:

Name Type Description Default
H_ext float

External applied field magnitude.

required
Ms float

Saturation magnetization.

required
chi float

Dimensionless initial susceptibility.

required
N float

Demagnetizing factor (default 0.5).

0.5

Returns:

Type Description
tuple

(M_solution, H_int, converged).

Source code in src/pymagnet/utils/_demag.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
@njit(cache=True)
def solve_demag_tanh(H_ext, Ms, chi, N=0.5):
    """Solve demagnetization for the tanh model (single scalar H_ext).

    Args:
        H_ext (float): External applied field magnitude.
        Ms (float): Saturation magnetization.
        chi (float): Dimensionless initial susceptibility.
        N (float): Demagnetizing factor (default 0.5).

    Returns:
        tuple: (M_solution, H_int, converged).
    """
    M_sol, converged = _brentq_njit(H_ext, N, Ms, chi, 0.0, Ms)
    H_int = H_ext - N * M_sol
    return M_sol, H_int, converged

solve_demag_tanh_batch(H_ext_array, Ms, chi, N=0.5)

Solve demagnetization for an array of H_ext values in parallel.

Parameters:

Name Type Description Default
H_ext_array ndarray

1D array of external field values.

required
Ms float

Saturation magnetization.

required
chi float

Dimensionless initial susceptibility.

required
N float

Demagnetizing factor (default 0.5).

0.5

Returns:

Type Description
tuple

(M_solutions, H_int_solutions) arrays of the same shape as H_ext_array.

Source code in src/pymagnet/utils/_demag.py
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
@njit(parallel=True, cache=True)
def solve_demag_tanh_batch(H_ext_array, Ms, chi, N=0.5):
    """Solve demagnetization for an array of H_ext values in parallel.

    Args:
        H_ext_array (ndarray): 1D array of external field values.
        Ms (float): Saturation magnetization.
        chi (float): Dimensionless initial susceptibility.
        N (float): Demagnetizing factor (default 0.5).

    Returns:
        tuple: (M_solutions, H_int_solutions) arrays of the same shape as
            H_ext_array.
    """
    n = H_ext_array.shape[0]
    M_out = _np.empty(n)
    H_int_out = _np.empty(n)

    for i in prange(n):  # ty:ignore[not-iterable]
        M_sol, H_int, _ = solve_demag_tanh(H_ext_array[i], Ms, chi, N)
        M_out[i] = M_sol
        H_int_out[i] = H_int

    return M_out, H_int_out

solve_demagnetization(H_ext, MH_interp, M_sat, N=0.5)

Solve the self-consistent demagnetization equation.

Finds M such that M = f_MH(H_ext - N * M) by solving g(M) = M - f_MH(H_ext - N * M) = 0.

Parameters:

Name Type Description Default
H_ext float

External applied field magnitude.

required
MH_interp Callable

Callable mapping H -> M. Can be a PchipInterpolator from build_MH_interpolator or an analytical model from tanh_MH_model.

required
M_sat float

Saturation magnetization (upper bracket bound).

required
N float

Demagnetizing factor. Default 0.5 (2D circle / infinite cylinder).

0.5

Returns:

Type Description
DemagResult

DemagResult with M_solution, H_int, convergence flag, and solver used.

Source code in src/pymagnet/utils/_demag.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
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
def solve_demagnetization(
    H_ext: float,
    MH_interp: Callable,
    M_sat: float,
    N: float = 0.5,
) -> DemagResult:
    """Solve the self-consistent demagnetization equation.

    Finds M such that M = f_MH(H_ext - N * M) by solving
    g(M) = M - f_MH(H_ext - N * M) = 0.

    Args:
        H_ext: External applied field magnitude.
        MH_interp: Callable mapping H -> M. Can be a PchipInterpolator from
            build_MH_interpolator or an analytical model from tanh_MH_model.
        M_sat: Saturation magnetization (upper bracket bound).
        N: Demagnetizing factor. Default 0.5 (2D circle / infinite cylinder).

    Returns:
        DemagResult with M_solution, H_int, convergence flag, and solver used.
    """
    if isinstance(MH_interp, PchipInterpolator):
        H_max = MH_interp.x[-1]
        if H_ext > H_max:
            warnings.warn(
                f"H_ext={H_ext} exceeds interpolation domain max={H_max}; "
                f"extrapolation will be used",
                stacklevel=2,
            )

    def residual(M):
        M_val = _np.asarray(M, dtype=float)
        result = M_val - MH_interp(H_ext - N * M_val)
        return float(result) if result.ndim == 0 else result

    g0 = residual(0.0)
    g_sat = residual(M_sat)

    # g0 == 0 means M=0 is the solution; g_sat == 0 means M=M_sat is the solution
    if abs(g0) < 1e-12:
        return DemagResult(
            M_solution=0.0, H_int=H_ext, converged=True, solver_used="exact"
        )
    if abs(g_sat) < 1e-12:
        return DemagResult(
            M_solution=M_sat,
            H_int=H_ext - N * M_sat,
            converged=True,
            solver_used="exact",
        )

    if g0 * g_sat < 0:
        M_solution = brentq(residual, 0.0, M_sat, xtol=1e-6, rtol=1e-6)
        converged = True
        solver_used = "brentq"
    else:
        warnings.warn(
            "Bracket [0, M_sat] invalid for brentq; falling back to fsolve",
            stacklevel=2,
        )
        sol, _info, ier, _msg = fsolve(residual, x0=M_sat / 2, full_output=True)
        M_solution = float(sol[0])
        converged = ier == 1
        solver_used = "fsolve"

    H_int = H_ext - N * M_solution
    return DemagResult(
        M_solution=M_solution,
        H_int=H_int,
        converged=converged,
        solver_used=solver_used,
    )

tanh_MH_model(Ms, chi)

Return an analytical M(H) function: M = Ms * tanh(chi * H / Ms).

At low fields this gives M โ‰ˆ chi * H (linear regime), and saturates smoothly to Ms at high fields.

Parameters:

Name Type Description Default
Ms float

Saturation magnetization.

required
chi float

Dimensionless initial susceptibility (slope dM/dH at H=0).

required

Returns:

Type Description
Callable

mapping H (float or array) -> M.

Source code in src/pymagnet/utils/_demag.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def tanh_MH_model(Ms: float, chi: float):
    """Return an analytical M(H) function: M = Ms * tanh(chi * H / Ms).

    At low fields this gives M โ‰ˆ chi * H (linear regime), and saturates
    smoothly to Ms at high fields.

    Args:
        Ms (float): Saturation magnetization.
        chi (float): Dimensionless initial susceptibility (slope dM/dH at H=0).

    Returns:
        Callable: mapping H (float or array) -> M.
    """

    def f_MH(H):
        return Ms * _np.tanh(chi * _np.asarray(H, dtype=float) / Ms)

    return f_MH

Routines for converting between coordinate systems, between 2D cartesian and polar, as well as 3D cartesian, cylindrical, and spherical.

cart2pol(x, y)

Converts from cartesian to polar coordinates

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required

Returns:

Type Description
tuple

rho, phi

Source code in src/pymagnet/utils/_conversions.py
15
16
17
18
19
20
21
22
23
24
25
26
27
def cart2pol(x: Numeric64, y: Numeric64) -> Tuple2_64:
    """Converts from cartesian to polar coordinates

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

    Returns:
        tuple: rho, phi
    """
    rho: Numeric64 = _np.sqrt(x**2 + y**2)
    phi: Numeric64 = _np.arctan2(y, x)
    return rho, phi

cart2sph(x, y, z)

Converts from cartesian to spherical coordinates

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
tuple

r, theta, phi

Source code in src/pymagnet/utils/_conversions.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def cart2sph(x: Numeric64, y: Numeric64, z: Numeric64) -> Tuple3_64:
    """Converts from cartesian to spherical coordinates

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

    Returns:
        tuple: r, theta, phi
    """
    r: Numeric64 = _np.sqrt(x**2 + y**2 + z**2)
    phi: Numeric64 = _np.arctan2(y, x)

    # 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"):
        theta: Numeric64 = _np.arccos(z / r)
    return (r, theta, phi)

get_unit_value_meter(unit)

Returns a queried metre unit as a number Example: factor = get_unit_value_meter('cm') print(f"factor for 'cm' is {factor}")

Parameters:

Name Type Description Default
unit string

SI length unit

required

Returns:

Type Description
float

SI prefix factor

Source code in src/pymagnet/utils/_conversions.py
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_unit_value_meter(unit: str) -> float | None:
    """Returns a queried metre unit as a number
    Example:
        factor = get_unit_value_meter('cm')
        print(f"factor for 'cm' is {factor}")

    Args:
        unit (string): SI length unit

    Returns:
        float: SI prefix factor
    """
    si_prefixes = {
        "Ym": 1e24,
        "Zm": 1e21,
        "Em": 1e18,
        "Pm": 1e15,
        "Tm": 1e12,
        "Gm": 1e9,
        "Mm": 1e6,
        "km": 1e3,
        "hm": 1e2,
        "dam": 1e1,
        "m": 1.0,
        "dm": 1e-1,
        "cm": 1e-2,
        "mm": 1e-3,
        "ยตm": 1e-6,
        "um": 1e-6,
        "nm": 1e-9,
        "Ang": 1e-10,
        "pm": 1e-12,
        "fm": 1e-15,
        "am": 1e-18,
        "zm": 1e-21,
        "ym": 1e-24,
    }

    return si_prefixes.get(unit)

get_unit_value_tesla(unit)

Returns a queried magnetic flux density unit as a number Example: factor = get_unit_value_meter('mT') print(f"factor for 'mT' is {factor}")

Parameters:

Name Type Description Default
unit string

SI length unit

required

Returns:

Type Description
float

SI prefix factor

Source code in src/pymagnet/utils/_conversions.py
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
def get_unit_value_tesla(unit: str) -> float | None:
    """Returns a queried magnetic flux density unit as a number
    Example:
        factor = get_unit_value_meter('mT')
        print(f"factor for 'mT' is {factor}")

    Args:
        unit (string): SI length unit

    Returns:
        float: SI prefix factor
    """

    si_prefixes = {
        "YT": 1e24,
        "ZT": 1e21,
        "ET": 1e18,
        "PT": 1e15,
        "TT": 1e12,
        "GT": 1e9,
        "MT": 1e6,
        "kT": 1e3,
        "hT": 1e2,
        "daT": 1e1,
        "T": 1,
        "dT": 1e-1,
        "cT": 1e-2,
        "mT": 1e-3,
        "ยตT": 1e-6,
        "uT": 1e-6,
        "nT": 1e-9,
        "pT": 1e-12,
        "fT": 1e-15,
        "aT": 1e-18,
        "zT": 1e-21,
        "yT": 1e-24,
    }

    return si_prefixes.get(unit)

pol2cart(rho, phi)

Converts from polar to cartesian coordinates

Parameters:

Name Type Description Default
rho ndarray

radial coordinates

required
phi ndarray

azimuthal coordinates

required

Returns:

Type Description
tuple

x,y

Source code in src/pymagnet/utils/_conversions.py
30
31
32
33
34
35
36
37
38
39
40
41
42
def pol2cart(rho: Numeric64, phi: Numeric64) -> Tuple2_64:
    """Converts from polar to cartesian coordinates

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

    Returns:
        tuple: x,y
    """
    x: Numeric64 = rho * _np.cos(phi)
    y: Numeric64 = rho * _np.sin(phi)
    return x, y

sph2cart(r, theta, phi)

Converts from spherical to cartesian coordinates

Parameters:

Name Type Description Default
r ndarray

radial coordinates

required
theta ndarray

azimuthal angles

required
phi ndarray

polar angle

required

Returns:

Type Description
tuple

x,y,z

Source code in src/pymagnet/utils/_conversions.py
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def sph2cart(r: Numeric64, theta: Numeric64, phi: Numeric64) -> Tuple3_64:
    """Converts from spherical to cartesian coordinates

    Args:
        r (ndarray): radial coordinates
        theta (ndarray): azimuthal angles
        phi (ndarray): polar angle

    Returns:
        tuple: x,y,z
    """
    x: Numeric64 = r * _np.sin(theta) * _np.cos(phi)
    y: Numeric64 = r * _np.sin(theta) * _np.sin(phi)
    z: Numeric64 = r * _np.cos(theta)
    return x, y, z

sphere_sph2cart(Br, Btheta, theta, phi)

Converts magnetic field of a sphere from spherical to cartesian coordinates

Parameters:

Name Type Description Default
Br ndarray

radial vector component

required
Btheta ndarray

polar vector component

required
theta ndarray

azimuthal angles

required
phi ndarray

polar angle

required

Returns:

Type Description
tuple

Bx,By,Bz

Source code in src/pymagnet/utils/_conversions.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def sphere_sph2cart(
    Br: Numeric64, Btheta: Numeric64, theta: Numeric64, phi: Numeric64
) -> Tuple3_64:
    """Converts magnetic field of a sphere from spherical to cartesian coordinates

    Args:
        Br (ndarray): radial vector component
        Btheta (ndarray): polar vector component
        theta (ndarray): azimuthal angles
        phi (ndarray): polar angle

    Returns:
        tuple: Bx,By,Bz
    """
    Bx: Numeric64 = Br * _np.sin(theta) * _np.cos(phi) + Btheta * _np.cos(
        theta
    ) * _np.cos(phi)

    By: Numeric64 = Br * _np.sin(theta) * _np.sin(phi) + Btheta * _np.cos(
        theta
    ) * _np.sin(phi)

    Bz: Numeric64 = Br * _np.cos(theta) - Btheta * _np.sin(theta)
    return Bx, By, Bz

vector_pol2cart(Brho, Bphi, phi)

Converts Vectors from polar to cartesian coordinates

Parameters:

Name Type Description Default
Brho ndarray

radial vector component

required
Bphi ndarray

azimuthal vector component

required
phi ndarray

azimuthal coordinates

required

Returns:

Type Description
tuple

Bx, By

Source code in src/pymagnet/utils/_conversions.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def vector_pol2cart(Brho: Numeric64, Bphi: Numeric64, phi: Numeric64) -> Tuple2_64:
    """Converts Vectors from polar to cartesian coordinates

    Args:
        Brho (ndarray): radial vector component
        Bphi (ndarray): azimuthal vector component
        phi (ndarray): azimuthal coordinates

    Returns:
        tuple: Bx, By
    """
    Bx: Numeric64 = Brho * _np.cos(phi) - Bphi * _np.sin(phi)
    By: Numeric64 = Brho * _np.sin(phi) + Bphi * _np.cos(phi)
    return Bx, By

vector_sph2cart(Br, Btheta, Bphi, theta, phi)

Converts Vectors from spherical to cartesian coordinates

Parameters:

Name Type Description Default
Br ndarray

radial vector component

required
Btheta ndarray

polar vector component

required
Bphi ndarray

azimuthal vector component

required
theta ndarray

azimuthal angles

required
phi ndarray

polar angle

required

Returns:

Type Description
tuple

Bx,By,Bz

Source code in src/pymagnet/utils/_conversions.py
 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
def vector_sph2cart(
    Br: Numeric64, Btheta: Numeric64, Bphi: Numeric64, theta: Numeric64, phi: Numeric64
) -> Tuple3_64:
    """Converts Vectors from spherical to cartesian coordinates

    Args:
        Br (ndarray): radial vector component
        Btheta (ndarray): polar vector component
        Bphi (ndarray): azimuthal vector component
        theta (ndarray): azimuthal angles
        phi (ndarray): polar angle

    Returns:
        tuple: Bx,By,Bz
    """
    Bx: Numeric64 = (
        Br * _np.sin(theta) * _np.cos(phi)
        + Btheta * _np.cos(theta) * _np.cos(phi)
        - Bphi * _np.sin(phi)
    )

    By: Numeric64 = (
        Br * _np.sin(theta) * _np.sin(phi)
        + Btheta * _np.cos(theta) * _np.sin(phi)
        + Bphi * _np.cos(phi)
    )

    Bz: Numeric64 = Br * _np.cos(theta) - Btheta * _np.sin(theta)
    return Bx, By, Bz

pymagnets.utils._point_structs

Private module consiting of point classes and their methods.

Point2

2D point class

Note that multiplication of two points is done elementwise, dot product is a separate method.

Source code in src/pymagnet/utils/_point_structs.py
 13
 14
 15
 16
 17
 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
 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
class Point2:
    """2D point class

    Note that multiplication of two points is done elementwise, dot product is
    a separate method.
    """

    def __init__(self, x, y):
        """Init method

        Args:
            x (ndarray): x coordinates
            y (ndarray): y coordinates
        """
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"({self.x}, {self.y})"

    def __str__(self) -> str:
        return f"({self.x}, {self.y})"

    def __add__(self, other):
        return Point2(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Point2(self.x - other.x, self.y - other.y)

    def __mul__(self, other):
        return Point2(self.x * other.x, self.y * other.y)

    def __div__(self, other):
        x = (self.x / other.x) if other.x != 0 else 0
        y = (self.y / other.y) if other.y != 0 else 0
        return Point2(x, y)

    def __lt__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag < other_mag

    def __le__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag <= other_mag

    def __gt__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag > other_mag

    def __ge__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag >= other_mag

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __ne__(self, other):
        return self.x != other.x or self.y != other.y

    def distance_to(self, point):
        """Calculates distance to a point

        Args:
            point (Point2): Target point

        Returns:
            float: distance
        """
        return _np.sqrt(_np.power(point.x - self.x, 2) + _np.power(point.y - self.y, 2))

    def distance_to_origin(self):
        """Calculates distance to a origin

        Returns:
            float: distance
        """
        return self._norm()

    def _norm(self):
        """Norm of Point

        Returns:
            float: norm
        """
        return _np.linalg.norm([self.x, self.y], axis=0)

__init__(x, y)

Init method

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required
Source code in src/pymagnet/utils/_point_structs.py
20
21
22
23
24
25
26
27
28
def __init__(self, x, y):
    """Init method

    Args:
        x (ndarray): x coordinates
        y (ndarray): y coordinates
    """
    self.x = x
    self.y = y

distance_to(point)

Calculates distance to a point

Parameters:

Name Type Description Default
point Point2

Target point

required

Returns:

Type Description
float

distance

Source code in src/pymagnet/utils/_point_structs.py
76
77
78
79
80
81
82
83
84
85
def distance_to(self, point):
    """Calculates distance to a point

    Args:
        point (Point2): Target point

    Returns:
        float: distance
    """
    return _np.sqrt(_np.power(point.x - self.x, 2) + _np.power(point.y - self.y, 2))

distance_to_origin()

Calculates distance to a origin

Returns:

Type Description
float

distance

Source code in src/pymagnet/utils/_point_structs.py
87
88
89
90
91
92
93
def distance_to_origin(self):
    """Calculates distance to a origin

    Returns:
        float: distance
    """
    return self._norm()

Point3

Bases: Point2

3D point class

Note that multiplication of two points is done elementwise, dot product is a separate method.

Source code in src/pymagnet/utils/_point_structs.py
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
class Point3(Point2):
    """3D point class

    Note that multiplication of two points is done elementwise, dot product is
    a separate method.

    """

    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z

    def __repr__(self) -> str:
        return f"({self.x}, {self.y}, {self.z})"

    def __str__(self) -> str:
        return f"({self.x}, {self.y}, {self.z})"

    def __add__(self, other):
        return Point3(self.x + other.x, self.y + other.y, self.z + other.z)

    def __sub__(self, other):
        return Point3(self.x - other.x, self.y - other.y, self.z - other.z)

    def __mul__(self, other):
        return Point3(self.x * other.x, self.y * other.y, self.z * other.z)

    def __div__(self, other):
        x = (self.x / other.x) if other.x != 0 else 0
        y = (self.y / other.y) if other.y != 0 else 0
        z = (self.z / other.z) if other.z != 0 else 0
        return Point3(x, y, z)

    def __lt__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag < other_mag

    def __le__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag <= other_mag

    def __gt__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag > other_mag

    def __ge__(self, other):
        self_mag = (self.x**2) + (self.y**2)
        other_mag = (other.x**2) + (other.y**2)
        return self_mag >= other_mag

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __ne__(self, other):
        return self.x != other.x or self.y != other.y

    def distance_to(self, point):
        return _np.sqrt(
            _np.power(point.x - self.x, 2)
            + _np.power(point.y - self.y, 2)
            + _np.power(point.z - self.z, 2)
        )

    def _norm(self):
        return _np.linalg.norm([self.x, self.y, self.z], axis=0)

    def distance_to_origin(self):
        return self._norm()

Quaternion module

Implements quaternion multiplication for convenient rotation of vectors in 3D.

Example

Rotation of a vector about the x-axis:

import numpy as np
import pymagnet as pm
vector1 = np.array([1,0,0])
rotate_about_z = pm.magnets.q_angle_from_axis(np.pi/2, (0, 0, 1))
vector2 = rotate_about_z * vector1

Quaternion

Quaternion class. overloading of multiplication symbol allows easy quaternion multiplications

Source code in src/pymagnet/utils/_quaternion.py
 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
class Quaternion:
    """Quaternion class.
    overloading of multiplication symbol allows easy quaternion multiplications

    """

    def __init__(
        self,
        w: _np.ndarray | float = 1.0,
        x: _np.ndarray | float = 0.0,
        y: _np.ndarray | float = 0.0,
        z: _np.ndarray | float = 0.0,
    ):
        """Initialse a pure quaternion (1; 0, 0, 0)

        Args:
            w (ndarray, optional): scalar quaternion. Defaults to 1.0.
            x (ndarray, optional): vector component. Defaults to 0.0.
            y (ndarray, optional): vector component. Defaults to 0.0.
            z (ndarray, optional): vector component. Defaults to 0.0.
        """
        self.w = _np.asarray(w)
        self.x = _np.asarray(x)
        self.y = _np.asarray(y)
        self.z = _np.asarray(z)

    def get_conjugate(self) -> Self:
        """Returns quaternion conjugate

        Returns:
            quaternion: quaternion conjugate
        """
        return Quaternion(self.w, -self.x, -self.y, -self.z)  # type: ignore

    @staticmethod
    def gen_rotation_quaternion(alpha_rad=0.0, beta_rad=0.0, gamma_rad=0.0):
        """Generates quaternion for rotation around z, y, x axes

        Args:
            alpha_rad (float): angle to z-axis. Defaults to 0.0.
            beta_rad (float): angle to y-axis. Defaults to 0.0.
            gamma_rad (float): angle to x-axis. Defaults to 0.0.

        Returns:
            Quaternion: rotation quaternion
        """

        rotate_about_x = Quaternion()
        rotate_about_y = Quaternion()
        rotate_about_z = Quaternion()

        forward_rotation = Quaternion()

        if _np.fabs(alpha_rad) > MAG_TOL:
            rotate_about_z = q_angle_from_axis(alpha_rad, (0, 0, 1))

        if _np.fabs(beta_rad) > MAG_TOL:
            rotate_about_y = q_angle_from_axis(beta_rad, (0, 1, 0))

        if _np.fabs(gamma_rad) > MAG_TOL:
            rotate_about_x = q_angle_from_axis(gamma_rad, (1, 0, 0))

        forward_rotation = rotate_about_x * rotate_about_z * rotate_about_y  # type: ignore

        return forward_rotation

    @staticmethod
    def _prepare_vector(x, y, z):
        """Creates 3xN array where each column corresponds to x, y, z vectors
        all of the same length. If some vectors are shorter, their values are repeated
        to ensure and array of (x,y,z) points for quaternion rotation.

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

        Returns:
            ndarray: 3xN numpy ndarray
        """
        # Check input x,y,z are numpy arrays and if not, convert to numpy arrays
        x = _np.asarray(x)
        y = _np.asarray(y)
        z = _np.asarray(z)

        longest_array_length = _np.max([x.size, y.size, z.size])

        # Ensure the unravelled arrays are all of the same length
        x = Quaternion._check_extend_array(x.ravel(), longest_array_length)
        y = Quaternion._check_extend_array(y.ravel(), longest_array_length)
        z = Quaternion._check_extend_array(z.ravel(), longest_array_length)

        return _np.array([x, y, z])

    @staticmethod
    def _check_extend_array(array, max):
        """Extend a 1D vector to length 'max' if it is shorter than 'max'

        Args:
            array (ndarray): numpy array
            max (int): array length to compare to

        Returns:
            ndarray: numpy array of length 'max'
        """

        if max % array.size != 0:
            raise ValueError("Incorrect gridding of x,y,z")
        # If array is smaller than longest array length 'max', extend it by tiling
        if array.size < max:
            extended_array = _np.tile(array, (max // array.size))

            # # If there is a remainder, 'n' append the first n elements of the array
            # # to ensure the final length is 'max'.
            # if max % array.size != 0:
            #     extended_array = _np.append(
            #         extended_array, array[0 : (max % array.size)]
            #     )
            return extended_array
        else:
            return array

    @staticmethod
    def _normalise_axis(vec):
        """Normalise

        Args:
            v (array): [description]

        Returns:
            [type]: [description]
        """
        vec = _np.asarray(vec)
        if _np.fabs(_np.linalg.norm(vec, axis=0)) < FP_CUTOFF:
            raise ValueError("Vec norm should be non-zero")
        return vec / _np.linalg.norm(vec, axis=0)

    @staticmethod
    def vec_norm(x, y, z):
        """Normalises each x,y,z vector

        Args:
            x (ndarray): x array
            y (ndarray): y array
            z (ndarray): z array

        Returns:
            array: 3xN array of normalised vectors
        """
        vec = Quaternion._prepare_vector(x, y, z)
        return _np.linalg.norm(vec, axis=0)

    def __repr__(self):
        str = f"({self.w}; {self.x}, {self.y}, {self.z})"
        return str

    def __mul__(self, b):
        if isinstance(b, Quaternion):
            return self._multiply_with_quaternion(b)
        elif isinstance(b, (list, tuple, _np.ndarray)):
            if len(b) != 3:
                raise ValueError(f"Input vector has invalid length {len(b)}")
            return self._multiply_with_vector(b)
        else:
            raise TypeError(f"Multiplication with unknown type {type(b)}")

    def as_tuple(self):
        """Returns quaternion as tuple of arrays

        Returns:
            tuple: w (array), x (array), y (array), z (array)
        """
        return self.w, self.x, self.y, self.z

    def _multiply_with_quaternion(self, q2):
        """Computes the Hamilton product of two quaternions

        Args:
            q2 (quaternion): quaternion

        Returns:
            quaternion: Hamilton product
        """
        w1, x1, y1, z1 = self.as_tuple()
        w2, x2, y2, z2 = q2.as_tuple()
        w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
        x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
        y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2
        z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
        result = Quaternion(w, x, y, z)
        return result

    def _multiply_with_vector(self, v):
        """Converts vector ``v`` to quaternion ``q2`` and then performs rotation by
        multiplying `q * q2 * q'`


        Args:
            v (vector/array): vector to be rotated

        Returns:
            tuple: multiplied vector x', y', z'
        """
        q2 = Quaternion(_np.zeros_like(v[0]), v[0], v[1], v[2])
        assert q2 is not None
        result = self * q2 * self.get_conjugate()
        return result.x, result.y, result.z

    def get_axisangle(self):
        theta = _np.arccos(self.w) * 2.0
        vec = _np.hstack([self.x, self.y, self.z])
        return theta, self._normalise_axis(vec)

    @staticmethod
    def euler_to_quaternion(alpha, beta, gamma):
        """Converts Euler angles to quaternion

        Args:
            alpha (float): angle to z-axis
            beta (float): angle to y-axis
            gamma (float): angle to x-axis

        Returns:
            Quaternion: Euler angles as a Quaternion
        """

        qw = _np.cos(alpha / 2) * _np.cos(beta / 2) * _np.cos(gamma / 2) + _np.sin(
            alpha / 2
        ) * _np.sin(beta / 2) * _np.sin(gamma / 2)
        qx = _np.sin(alpha / 2) * _np.cos(beta / 2) * _np.cos(gamma / 2) - _np.cos(
            alpha / 2
        ) * _np.sin(beta / 2) * _np.sin(gamma / 2)
        qy = _np.cos(alpha / 2) * _np.sin(beta / 2) * _np.cos(gamma / 2) + _np.sin(
            alpha / 2
        ) * _np.cos(beta / 2) * _np.sin(gamma / 2)
        qz = _np.cos(alpha / 2) * _np.cos(beta / 2) * _np.sin(gamma / 2) - _np.sin(
            alpha / 2
        ) * _np.sin(beta / 2) * _np.cos(gamma / 2)

        return Quaternion(qw, qx, qy, qz)

__init__(w=1.0, x=0.0, y=0.0, z=0.0)

Initialse a pure quaternion (1; 0, 0, 0)

Parameters:

Name Type Description Default
w ndarray

scalar quaternion. Defaults to 1.0.

1.0
x ndarray

vector component. Defaults to 0.0.

0.0
y ndarray

vector component. Defaults to 0.0.

0.0
z ndarray

vector component. Defaults to 0.0.

0.0
Source code in src/pymagnet/utils/_quaternion.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def __init__(
    self,
    w: _np.ndarray | float = 1.0,
    x: _np.ndarray | float = 0.0,
    y: _np.ndarray | float = 0.0,
    z: _np.ndarray | float = 0.0,
):
    """Initialse a pure quaternion (1; 0, 0, 0)

    Args:
        w (ndarray, optional): scalar quaternion. Defaults to 1.0.
        x (ndarray, optional): vector component. Defaults to 0.0.
        y (ndarray, optional): vector component. Defaults to 0.0.
        z (ndarray, optional): vector component. Defaults to 0.0.
    """
    self.w = _np.asarray(w)
    self.x = _np.asarray(x)
    self.y = _np.asarray(y)
    self.z = _np.asarray(z)

as_tuple()

Returns quaternion as tuple of arrays

Returns:

Type Description
tuple

w (array), x (array), y (array), z (array)

Source code in src/pymagnet/utils/_quaternion.py
193
194
195
196
197
198
199
def as_tuple(self):
    """Returns quaternion as tuple of arrays

    Returns:
        tuple: w (array), x (array), y (array), z (array)
    """
    return self.w, self.x, self.y, self.z

euler_to_quaternion(alpha, beta, gamma) staticmethod

Converts Euler angles to quaternion

Parameters:

Name Type Description Default
alpha float

angle to z-axis

required
beta float

angle to y-axis

required
gamma float

angle to x-axis

required

Returns:

Type Description
Quaternion

Euler angles as a Quaternion

Source code in src/pymagnet/utils/_quaternion.py
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
@staticmethod
def euler_to_quaternion(alpha, beta, gamma):
    """Converts Euler angles to quaternion

    Args:
        alpha (float): angle to z-axis
        beta (float): angle to y-axis
        gamma (float): angle to x-axis

    Returns:
        Quaternion: Euler angles as a Quaternion
    """

    qw = _np.cos(alpha / 2) * _np.cos(beta / 2) * _np.cos(gamma / 2) + _np.sin(
        alpha / 2
    ) * _np.sin(beta / 2) * _np.sin(gamma / 2)
    qx = _np.sin(alpha / 2) * _np.cos(beta / 2) * _np.cos(gamma / 2) - _np.cos(
        alpha / 2
    ) * _np.sin(beta / 2) * _np.sin(gamma / 2)
    qy = _np.cos(alpha / 2) * _np.sin(beta / 2) * _np.cos(gamma / 2) + _np.sin(
        alpha / 2
    ) * _np.cos(beta / 2) * _np.sin(gamma / 2)
    qz = _np.cos(alpha / 2) * _np.cos(beta / 2) * _np.sin(gamma / 2) - _np.sin(
        alpha / 2
    ) * _np.sin(beta / 2) * _np.cos(gamma / 2)

    return Quaternion(qw, qx, qy, qz)

gen_rotation_quaternion(alpha_rad=0.0, beta_rad=0.0, gamma_rad=0.0) staticmethod

Generates quaternion for rotation around z, y, x axes

Parameters:

Name Type Description Default
alpha_rad float

angle to z-axis. Defaults to 0.0.

0.0
beta_rad float

angle to y-axis. Defaults to 0.0.

0.0
gamma_rad float

angle to x-axis. Defaults to 0.0.

0.0

Returns:

Type Description
Quaternion

rotation quaternion

Source code in src/pymagnet/utils/_quaternion.py
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
@staticmethod
def gen_rotation_quaternion(alpha_rad=0.0, beta_rad=0.0, gamma_rad=0.0):
    """Generates quaternion for rotation around z, y, x axes

    Args:
        alpha_rad (float): angle to z-axis. Defaults to 0.0.
        beta_rad (float): angle to y-axis. Defaults to 0.0.
        gamma_rad (float): angle to x-axis. Defaults to 0.0.

    Returns:
        Quaternion: rotation quaternion
    """

    rotate_about_x = Quaternion()
    rotate_about_y = Quaternion()
    rotate_about_z = Quaternion()

    forward_rotation = Quaternion()

    if _np.fabs(alpha_rad) > MAG_TOL:
        rotate_about_z = q_angle_from_axis(alpha_rad, (0, 0, 1))

    if _np.fabs(beta_rad) > MAG_TOL:
        rotate_about_y = q_angle_from_axis(beta_rad, (0, 1, 0))

    if _np.fabs(gamma_rad) > MAG_TOL:
        rotate_about_x = q_angle_from_axis(gamma_rad, (1, 0, 0))

    forward_rotation = rotate_about_x * rotate_about_z * rotate_about_y  # type: ignore

    return forward_rotation

get_conjugate()

Returns quaternion conjugate

Returns:

Type Description
quaternion

quaternion conjugate

Source code in src/pymagnet/utils/_quaternion.py
53
54
55
56
57
58
59
def get_conjugate(self) -> Self:
    """Returns quaternion conjugate

    Returns:
        quaternion: quaternion conjugate
    """
    return Quaternion(self.w, -self.x, -self.y, -self.z)  # type: ignore

vec_norm(x, y, z) staticmethod

Normalises each x,y,z vector

Parameters:

Name Type Description Default
x ndarray

x array

required
y ndarray

y array

required
z ndarray

z array

required

Returns:

Type Description
array

3xN array of normalised vectors

Source code in src/pymagnet/utils/_quaternion.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
@staticmethod
def vec_norm(x, y, z):
    """Normalises each x,y,z vector

    Args:
        x (ndarray): x array
        y (ndarray): y array
        z (ndarray): z array

    Returns:
        array: 3xN array of normalised vectors
    """
    vec = Quaternion._prepare_vector(x, y, z)
    return _np.linalg.norm(vec, axis=0)

q_angle_from_axis(theta, vec)

Generates a rotation quaternion for an angle theta about an axis vec This is a normalised, i.e. unit quaternion.

Parameters:

Name Type Description Default
theta float

angle of rotation

required
vec tuple / array

axis vector

required
Example

90 degree rotation about the x axis:

rotation_quaternion = q_angle_from_axis(np.pi/2, (1, 0, 0) )

Returns:

Type Description
Quaternion

rotation quaternion

Source code in src/pymagnet/utils/_quaternion.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
def q_angle_from_axis(theta, vec) -> Quaternion:
    """Generates a rotation quaternion for an angle `theta` about an axis `vec`
    This is a normalised, i.e. unit quaternion.

    Args:
        theta (float): angle of rotation
        vec (tuple/array): axis vector

    Example:
        90 degree rotation about the x axis:

            rotation_quaternion = q_angle_from_axis(np.pi/2, (1, 0, 0) )

    Returns:
        Quaternion: rotation quaternion
    """
    vec = Quaternion._normalise_axis(vec)
    w = _np.cos(theta / 2.0)
    vec *= _np.sin(theta / 2.0)
    x = vec[0]
    y = vec[1]
    z = vec[2]
    rotation_quaternion = Quaternion(w, x, y, z)
    return rotation_quaternion

Routines for Two Dimensional Magnet Classes

BdotgradB_2D(B, x, y)

Computes (B . grad)B using the full Jacobian tensor.

Calculates the directional derivative of B along B

[(Bยทโˆ‡)B]_x = Bx * dBx/dx + By * dBx/dy [(Bยทโˆ‡)B]_y = Bx * dBy/dx + By * dBy/dy

This is the force-relevant quantity for magnetic gradient forces on paramagnetic materials.

Parameters:

Name Type Description Default
B Field2

Magnetic field vector with .x, .y components

required
x ndarray

x coordinates (2D grid)

required
y ndarray

y coordinates (2D grid)

required

Returns:

Type Description
Field2

(Bยทโˆ‡)B vector and its norm

Source code in src/pymagnet/utils/_routines2D.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def BdotgradB_2D(B, x, y):
    """Computes (B . grad)B using the full Jacobian tensor.

    Calculates the directional derivative of B along B:
        [(Bยทโˆ‡)B]_x = Bx * dBx/dx + By * dBx/dy
        [(Bยทโˆ‡)B]_y = Bx * dBy/dx + By * dBy/dy

    This is the force-relevant quantity for magnetic gradient forces
    on paramagnetic materials.

    Args:
        B (Field2): Magnetic field vector with .x, .y components
        x (ndarray): x coordinates (2D grid)
        y (ndarray): y coordinates (2D grid)

    Returns:
        Field2: (Bยทโˆ‡)B vector and its norm
    """
    J = jacobian_B_2D(B, x, y)
    F = Field2(_np.zeros_like(B.x), _np.zeros_like(B.y))
    F.x = B.x * J.dBx_dx + B.y * J.dBx_dy
    F.y = B.x * J.dBy_dx + B.y * J.dBy_dy
    F.calc_norm()
    return F

FgradB_2D(B, x, y, chi_m, c)

Calculates the magnetic field gradient force for a 2D field.

Computes F = (chi_m / mu_0) * c * |B| * grad(|B|). This is a scalar approximation of the gradient force.

Parameters:

Name Type Description Default
B Field2

Magnetic field vector (must have .n for magnitude)

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
chi_m float

Magnetic susceptibility

required
c float

Material constant

required

Returns:

Type Description
Field2

Magnetic field gradient force vector

Source code in src/pymagnet/utils/_routines2D.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def FgradB_2D(B, x, y, chi_m, c):
    """Calculates the magnetic field gradient force for a 2D field.

    Computes F = (chi_m / mu_0) * c * |B| * grad(|B|).
    This is a scalar approximation of the gradient force.

    Args:
        B (Field2): Magnetic field vector (must have .n for magnitude)
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        chi_m (float): Magnetic susceptibility
        c (float): Material constant

    Returns:
        Field2: Magnetic field gradient force vector
    """
    dB = gradB_2D(B.n, x, y)
    scale = (1 / MU0) * chi_m * c
    FB = Field2(_np.zeros_like(B.n), _np.zeros_like(B.n))
    FB.x = scale * dB.x * B.n
    FB.y = scale * dB.y * B.n
    FB.n = scale * dB.n * B.n
    return FB

get_field_2D(Point_Array2)

Calculates magnetic field at an array of points due to every instantated Magnet2D magnet.

Parameters:

Name Type Description Default
Point_Array2 Point_Array2

array of x,y points and associated unit, defaults to 'mm'

required

Returns:

Type Description
Field2

array of Bx,By,|B| values and associated unit (defaults to 'T')

Source code in src/pymagnet/utils/_routines2D.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def get_field_2D(Point_Array2):
    """Calculates magnetic field at an array of points due to every instantated
    `Magnet2D` magnet.

    Args:
        Point_Array2 (Point_Array2): array of x,y points and associated unit,
            defaults to 'mm'

    Returns:
        Field2: array of Bx,By,|B| values and associated unit (defaults to 'T')
    """
    from ..magnets import Magnet2D

    # Empty data structure
    B = _allocate_field_array2(Point_Array2.x, Point_Array2.y)

    for magnet in Magnet2D.instances:
        Bx, By = magnet.get_field(Point_Array2.x, Point_Array2.y)
        B.x += Bx
        B.y += By

    B.calc_norm()
    return B

gradB_2D(B, x, y)

Calculates the spatial gradient of the magnetic field magnitude.

Computes grad(|B|) using finite differences. Uses numba-accelerated kernels for grids >= 10k points, np.gradient for smaller grids.

Parameters:

Name Type Description Default
B ndarray

Magnetic field magnitude |B| (2D array)

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required

Returns:

Type Description
Field2

Gradient vector (d|B|/dx, d|B|/dy) and its norm

Source code in src/pymagnet/utils/_routines2D.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def gradB_2D(B, x, y):
    """Calculates the spatial gradient of the magnetic field magnitude.

    Computes grad(|B|) using finite differences. Uses numba-accelerated
    kernels for grids >= 10k points, np.gradient for smaller grids.

    Args:
        B (ndarray): Magnetic field magnitude |B| (2D array)
        x (ndarray): x coordinates
        y (ndarray): y coordinates

    Returns:
        Field2: Gradient vector (d|B|/dx, d|B|/dy) and its norm
    """
    dx, dy = _grid_spacing_2d(x, y)
    dB = Field2(_np.zeros_like(B), _np.zeros_like(B))
    if B.size >= _NUMBA_THRESHOLD:
        dB.x, dB.y = _gradient_2d(_np.ascontiguousarray(B), dx, dy)
    else:
        dB.x, dB.y = _np.gradient(B, dx, dy)
    dB.calc_norm()
    return dB

grid2D(xmax, ymax, **kwargs)

Generates grid of x and y points

Parameters:

Name Type Description Default
xmax float

maximum x value

required
ymax float

maximum y value

required
Kwargs

num_points (int): Number of points in each direction. Defaults to 100 xmin (float): minimum x value. Defaults to -xmax ymin (float): minimum y value. Defaults to -ymax unit (str): unit length. Defaults to 'mm'

Returns:

Type Description
Point_Array2

array of x and y values of shape (num_points, num_points) and associated unit

Source code in src/pymagnet/utils/_routines2D.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def grid2D(xmax, ymax, **kwargs):
    """Generates grid of x and y points

    Args:
        xmax (float): maximum x value
        ymax (float): maximum y value

    Kwargs:
        num_points (int): Number of points in each direction. Defaults to 100
        xmin (float): minimum x value. Defaults to -xmax
        ymin (float): minimum y value. Defaults to -ymax
        unit (str): unit length. Defaults to 'mm'

    Returns:
        Point_Array2: array of x and y values of shape (num_points, num_points)
            and associated unit
    """
    num_points = kwargs.pop("num_points", 100)
    xmin = kwargs.pop("xmin", -1 * xmax)
    ymin = kwargs.pop("ymin", -1 * ymax)
    unit = kwargs.pop("unit", "mm")
    NPJ = num_points * 1j
    x, y = _np.mgrid[xmin:xmax:NPJ, ymin:ymax:NPJ]
    return Point_Array2(x, y, unit=unit)

jacobian_B_2D(B, x, y)

Calculates the Jacobian of the 2D magnetic field vector.

Computes J_ij = dB_i/dx_j, the full tensor gradient of the vector field.

Parameters:

Name Type Description Default
B Field2

Magnetic field vector with .x, .y components

required
x ndarray

x coordinates (2D grid)

required
y ndarray

y coordinates (2D grid)

required

Returns:

Type Description
Jacobian2

Dataclass with components dBx_dx, dBx_dy, dBy_dx, dBy_dy

Source code in src/pymagnet/utils/_routines2D.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def jacobian_B_2D(B, x, y):
    """Calculates the Jacobian of the 2D magnetic field vector.

    Computes J_ij = dB_i/dx_j, the full tensor gradient of the vector field.

    Args:
        B (Field2): Magnetic field vector with .x, .y components
        x (ndarray): x coordinates (2D grid)
        y (ndarray): y coordinates (2D grid)

    Returns:
        Jacobian2: Dataclass with components dBx_dx, dBx_dy, dBy_dx, dBy_dy
    """
    dx, dy = _grid_spacing_2d(x, y)
    if B.x.size >= _NUMBA_THRESHOLD:
        dBx_dx, dBx_dy = _gradient_2d(_np.ascontiguousarray(B.x), dx, dy)
        dBy_dx, dBy_dy = _gradient_2d(_np.ascontiguousarray(B.y), dx, dy)
    else:
        dBx_dx, dBx_dy = _np.gradient(B.x, dx, dy)
        dBy_dx, dBy_dy = _np.gradient(B.y, dx, dy)
    return Jacobian2(dBx_dx=dBx_dx, dBx_dy=dBx_dy, dBy_dx=dBy_dx, dBy_dy=dBy_dy)

rotate_points_2D(x, y, alpha)

Counter-clockwise rotation of points x,y

Rotates 2D coordinates using a rotation matrix

Parameters:

Name Type Description Default
x ndarray

array of x coordinates

required
y ndarray

array of x coordinates

required
alpha float

rotation angle w.r.t. x-axis

required

Returns:

Type Description
tuple

(x', y') rotated array of points

Source code in src/pymagnet/utils/_routines2D.py
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
def rotate_points_2D(x, y, alpha):
    """Counter-clockwise rotation of points x,y

    Rotates 2D coordinates using a rotation matrix

    Args:
        x (ndarray): array of x coordinates
        y (ndarray): array of x coordinates
        alpha (float): rotation angle w.r.t. x-axis

    Returns:
        tuple: (x', y') rotated array of points
    """
    x = _np.atleast_1d(x)
    y = _np.atleast_1d(y)
    if len(x) != len(y):
        raise ValueError("Must have same number of points in x and y")

    rot_matrix = _np.array(
        [[_np.cos(alpha), -_np.sin(alpha)], [_np.sin(alpha), _np.cos(alpha)]]
    )
    stacked_points = _np.column_stack((_np.ravel(x), _np.ravel(y)))
    rotated_points = _np.dot(rot_matrix, stacked_points.T)
    x_rotated = rotated_points[0, :]
    y_rotated = rotated_points[1, :]

    return _np.reshape(x_rotated, x.shape), _np.reshape(y_rotated, y.shape)

Routines for Three Dimensional Magnet Classes

BdotgradB_3D(B, x, y, z)

Computes (B . grad)B using the full Jacobian tensor.

Calculates the directional derivative of B along B

[(Bยทโˆ‡)B]_x = Bx * dBx/dx + By * dBx/dy + Bz * dBx/dz [(Bยทโˆ‡)B]_y = Bx * dBy/dx + By * dBy/dy + Bz * dBy/dz [(Bยทโˆ‡)B]_z = Bx * dBz/dx + By * dBz/dy + Bz * dBz/dz

Supports both full 3D grids and 2D slices.

Parameters:

Name Type Description Default
B Field3

Magnetic field vector with .x, .y, .z components

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
Field3

(Bยทโˆ‡)B vector and its norm

Source code in src/pymagnet/utils/_routines3D.py
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
def BdotgradB_3D(B, x, y, z):
    """Computes (B . grad)B using the full Jacobian tensor.

    Calculates the directional derivative of B along B:
        [(Bยทโˆ‡)B]_x = Bx * dBx/dx + By * dBx/dy + Bz * dBx/dz
        [(Bยทโˆ‡)B]_y = Bx * dBy/dx + By * dBy/dy + Bz * dBy/dz
        [(Bยทโˆ‡)B]_z = Bx * dBz/dx + By * dBz/dy + Bz * dBz/dz

    Supports both full 3D grids and 2D slices.

    Args:
        B (Field3): Magnetic field vector with .x, .y, .z components
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates

    Returns:
        Field3: (Bยทโˆ‡)B vector and its norm
    """
    J = jacobian_B_3D(B, x, y, z)
    F = Field3(_np.zeros_like(B.x), _np.zeros_like(B.y), _np.zeros_like(B.z))
    F.x = B.x * J.dBx_dx + B.y * J.dBx_dy + B.z * J.dBx_dz
    F.y = B.x * J.dBy_dx + B.y * J.dBy_dy + B.z * J.dBy_dz
    F.z = B.x * J.dBz_dx + B.y * J.dBz_dy + B.z * J.dBz_dz
    F.calc_norm()
    return F

FgradB_3D(B, x, y, z, chi_m, c)

Calculates the magnetic field gradient force for a 3D field.

Computes F = (chi_m / mu_0) * c * |B| * grad(|B|). This is a scalar approximation of the gradient force.

Parameters:

Name Type Description Default
B Field3

Magnetic field vector (must have .n for magnitude)

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required
chi_m float

Magnetic susceptibility

required
c float

Material constant

required

Returns:

Type Description
Field3

Magnetic field gradient force vector

Source code in src/pymagnet/utils/_routines3D.py
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
def FgradB_3D(B, x, y, z, chi_m, c):
    """Calculates the magnetic field gradient force for a 3D field.

    Computes F = (chi_m / mu_0) * c * |B| * grad(|B|).
    This is a scalar approximation of the gradient force.

    Args:
        B (Field3): Magnetic field vector (must have .n for magnitude)
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates
        chi_m (float): Magnetic susceptibility
        c (float): Material constant

    Returns:
        Field3: Magnetic field gradient force vector
    """
    dB = gradB_3D(B.n, x, y, z)
    scale = (1 / MU0) * chi_m * c
    FB = Field3(_np.zeros_like(B.n), _np.zeros_like(B.n), _np.zeros_like(B.n))
    FB.x = scale * dB.x * B.n
    FB.y = scale * dB.y * B.n
    FB.z = scale * dB.z * B.n
    FB.n = scale * dB.n * B.n
    return FB

get_field_3D(points)

Calculates magnetic field at an array of points due to every instantated Magnet3D magnet.

Parameters:

Name Type Description Default
points Point_Array3

array of x,y,z points and associated unit, defaults to 'mm'

required

Returns:

Type Description
Field3

array of Bx,By,Bz,|B| values and associated unit (defaults to 'T')

Source code in src/pymagnet/utils/_routines3D.py
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
def get_field_3D(points):
    """Calculates magnetic field at an array of points due to every instantated
    `Magnet3D` magnet.

    Args:
        points (Point_Array3): array of x,y,z points and associated unit,
            defaults to 'mm'

    Returns:
        Field3: array of Bx,By,Bz,|B| values and associated unit (defaults to 'T')
    """
    from ..magnets import Magnet3D

    B = _allocate_field_array3(points.x, points.y, points.z)

    # Track which points are inside any magnet (for masking after summation)
    mask_magnet = _np.zeros(B.x.shape, dtype=bool)
    needs_masking = False

    for magnet in Magnet3D.instances:
        Bx, By, Bz = magnet.get_field(points.x, points.y, points.z)
        Bx = Bx.reshape(B.x.shape)
        By = By.reshape(B.y.shape)
        Bz = Bz.reshape(B.z.shape)

        # Replace NaN with zero before accumulation to prevent NaN
        # poisoning the sum (NaN + valid = NaN). NaN can arise from
        # mask_magnet flagging points inside a magnet, or from
        # numerical singularities at magnet edges.
        nan_mask = _np.isnan(Bx) | _np.isnan(By) | _np.isnan(Bz)
        if _np.any(nan_mask):
            Bx = _np.where(nan_mask, 0.0, Bx)
            By = _np.where(nan_mask, 0.0, By)
            Bz = _np.where(nan_mask, 0.0, Bz)

            if magnet._mask_magnet:
                mask_magnet |= nan_mask
                needs_masking = True

        B.x += Bx
        B.y += By
        B.z += Bz

    # Re-apply NaN mask for points inside any magnet
    if needs_masking:
        B.x[mask_magnet] = _np.nan
        B.y[mask_magnet] = _np.nan
        B.z[mask_magnet] = _np.nan

    B.calc_norm()
    return B

gradB_3D(B, x, y, z)

Calculates the spatial gradient of the magnetic field magnitude.

Computes grad(|B|) using finite differences. Uses numba-accelerated kernels for grids >= 10k points, np.gradient for smaller grids. Supports both full 3D grids (from grid3D) and 2D slices (from slice3D).

Parameters:

Name Type Description Default
B ndarray

Magnetic field magnitude |B|

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
Field3

Gradient vector (d|B|/dx, d|B|/dy, d|B|/dz) and its norm

Source code in src/pymagnet/utils/_routines3D.py
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
def gradB_3D(B, x, y, z):
    """Calculates the spatial gradient of the magnetic field magnitude.

    Computes grad(|B|) using finite differences. Uses numba-accelerated
    kernels for grids >= 10k points, np.gradient for smaller grids.
    Supports both full 3D grids (from grid3D) and 2D slices (from slice3D).

    Args:
        B (ndarray): Magnetic field magnitude |B|
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates

    Returns:
        Field3: Gradient vector (d|B|/dx, d|B|/dy, d|B|/dz) and its norm
    """
    B_arr = _np.ascontiguousarray(B)
    use_numba = B_arr.size >= _NUMBA_THRESHOLD

    if B_arr.ndim == 3:
        dx, dy, dz = _grid_spacing_3d(x, y, z)
        if use_numba:
            dBdx, dBdy, dBdz = _gradient_3d(B_arr, dx, dy, dz)
        else:
            dBdx, dBdy, dBdz = _np.gradient(B_arr, dx, dy, dz)
    elif B_arr.ndim == 2:
        varying, _constant, coords = _detect_slice_axes(x, y, z)
        c1, c2 = coords[varying[0]], coords[varying[1]]
        N1, N2 = B_arr.shape
        d1 = (c1.max() - c1.min()) / N1
        d2 = (c2.max() - c2.min()) / N2
        if use_numba:
            grad1, grad2 = _gradient_2d(B_arr, d1, d2)
        else:
            grad1, grad2 = _np.gradient(B_arr, d1, d2)

        components = {
            "x": _np.zeros_like(B_arr),
            "y": _np.zeros_like(B_arr),
            "z": _np.zeros_like(B_arr),
        }
        components[varying[0]] = grad1
        components[varying[1]] = grad2
        dBdx, dBdy, dBdz = components["x"], components["y"], components["z"]
    else:
        raise ValueError(f"B must be 2D (slice) or 3D (grid), got ndim={B_arr.ndim}")

    dB = Field3(dBdx, dBdy, dBdz)
    dB.calc_norm()
    return dB

grid3D(xmax, ymax, zmax, **kwargs)

Generates grid of x, y, z points

Parameters:

Name Type Description Default
xmax float

maximum x value

required
ymax float

maximum y value

required
zmax float

maximum y value

required
Kwargs

num_points (int): Number of points in each direction. Defaults to 100 xmin (float): minimum x value. Defaults to -xmax ymin (float): minimum y value. Defaults to -ymax zmin (float): minimum y value. Defaults to -zmax unit (string): unit length. Defaults to 'mm'

Returns:

Type Description
Point_Array2

array of x and y values of shape (num_points, num_points) and associated unit

Source code in src/pymagnet/utils/_routines3D.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
76
77
78
79
def grid3D(xmax, ymax, zmax, **kwargs):
    """Generates grid of x, y, z points

    Args:
        xmax (float): maximum x value
        ymax (float): maximum y value
        zmax (float): maximum y value

    Kwargs:
        num_points (int): Number of points in each direction. Defaults to 100
        xmin (float): minimum x value. Defaults to -xmax
        ymin (float): minimum y value. Defaults to -ymax
        zmin (float): minimum y value. Defaults to -zmax
        unit (string): unit length. Defaults to 'mm'

    Returns:
        Point_Array2: array of x and y values of shape (num_points, num_points) and associated unit
    """
    num_points = kwargs.pop("num_points", None)

    xmin = kwargs.pop("xmin", -1 * xmax)
    ymin = kwargs.pop("ymin", -1 * ymax)
    zmin = kwargs.pop("zmin", -1 * zmax)
    unit = kwargs.pop("unit", "mm")
    # NPJ = num_points * 1j

    if num_points is None:
        num_points_x = kwargs.pop("num_points_x", 100)
        num_points_y = kwargs.pop("num_points_y", 100)
        num_points_z = kwargs.pop("num_points_z", 100)
    else:
        num_points_x = num_points
        num_points_y = num_points
        num_points_z = num_points

    x, y, z = _np.mgrid[
        xmin : xmax : num_points_x * 1j,
        ymin : ymax : num_points_y * 1j,
        zmin : zmax : num_points_z * 1j,
    ]

    return Point_Array3(x, y, z, unit=unit)

jacobian_B_3D(B, x, y, z)

Calculates the Jacobian of the 3D magnetic field vector.

Computes J_ij = dB_i/dx_j, the full tensor gradient of the vector field. Supports both full 3D grids (from grid3D) and 2D slices (from slice3D).

Parameters:

Name Type Description Default
B Field3

Magnetic field vector with .x, .y, .z components

required
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required

Returns:

Type Description
Jacobian3

Dataclass with all 9 partial derivative components

Source code in src/pymagnet/utils/_routines3D.py
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
def jacobian_B_3D(B, x, y, z):
    """Calculates the Jacobian of the 3D magnetic field vector.

    Computes J_ij = dB_i/dx_j, the full tensor gradient of the vector field.
    Supports both full 3D grids (from grid3D) and 2D slices (from slice3D).

    Args:
        B (Field3): Magnetic field vector with .x, .y, .z components
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates

    Returns:
        Jacobian3: Dataclass with all 9 partial derivative components
    """
    Bx = _np.ascontiguousarray(B.x)
    By = _np.ascontiguousarray(B.y)
    Bz = _np.ascontiguousarray(B.z)
    use_numba = Bx.size >= _NUMBA_THRESHOLD

    if Bx.ndim == 3:
        dx, dy, dz = _grid_spacing_3d(x, y, z)
        if use_numba:
            dBx_dx, dBx_dy, dBx_dz = _gradient_3d(Bx, dx, dy, dz)
            dBy_dx, dBy_dy, dBy_dz = _gradient_3d(By, dx, dy, dz)
            dBz_dx, dBz_dy, dBz_dz = _gradient_3d(Bz, dx, dy, dz)
        else:
            dBx_dx, dBx_dy, dBx_dz = _np.gradient(Bx, dx, dy, dz)
            dBy_dx, dBy_dy, dBy_dz = _np.gradient(By, dx, dy, dz)
            dBz_dx, dBz_dy, dBz_dz = _np.gradient(Bz, dx, dy, dz)
    elif Bx.ndim == 2:
        varying, _constant, coords = _detect_slice_axes(x, y, z)
        c1, c2 = coords[varying[0]], coords[varying[1]]
        N1, N2 = Bx.shape
        d1 = (c1.max() - c1.min()) / N1
        d2 = (c2.max() - c2.min()) / N2

        zeros = _np.zeros_like(Bx)
        grad_fn = (
            _gradient_2d if use_numba else lambda F, d1, d2: _np.gradient(F, d1, d2)
        )
        # Compute gradients for each B component on the 2D slice
        result = {}
        for comp_name, comp_arr in [("Bx", Bx), ("By", By), ("Bz", Bz)]:
            g1, g2 = grad_fn(comp_arr, d1, d2)
            grads = {"x": zeros.copy(), "y": zeros.copy(), "z": zeros.copy()}
            grads[varying[0]] = g1
            grads[varying[1]] = g2
            result[comp_name] = grads

        dBx_dx, dBx_dy, dBx_dz = result["Bx"]["x"], result["Bx"]["y"], result["Bx"]["z"]
        dBy_dx, dBy_dy, dBy_dz = result["By"]["x"], result["By"]["y"], result["By"]["z"]
        dBz_dx, dBz_dy, dBz_dz = result["Bz"]["x"], result["Bz"]["y"], result["Bz"]["z"]
    else:
        raise ValueError(
            f"B components must be 2D (slice) or 3D (grid), got ndim={Bx.ndim}"
        )

    return Jacobian3(
        dBx_dx=dBx_dx,
        dBx_dy=dBx_dy,
        dBx_dz=dBx_dz,
        dBy_dx=dBy_dx,
        dBy_dy=dBy_dy,
        dBy_dz=dBy_dz,
        dBz_dx=dBz_dx,
        dBz_dy=dBz_dy,
        dBz_dz=dBz_dz,
    )

line3D(start, end, num_points=100, **kwargs)

Generates a line of points

Parameters:

Name Type Description Default
start tuple

Starting point (x1,y1,z1)

required
end tuple

End point (x2,y2,z2)

required
num_points int

number of points to generate. Defaults to 100

100

Other Parameters:

Name Type Description
unit str

length scale units. Defaults to "mm".

Returns:

Type Description
Point_Array3

array of x, y, and z values of shape (num_points) and associated unit

Source code in src/pymagnet/utils/_routines3D.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def line3D(start, end, num_points=100, **kwargs):
    """Generates a line of points

    Args:
        start (tuple): Starting point (x1,y1,z1)
        end (tuple): End point (x2,y2,z2)
        num_points (int): number of points to generate. Defaults to 100

    Other Parameters:
        unit (str): length scale units. Defaults to "mm".

    Returns:
        Point_Array3: array of x, y, and z values of shape (num_points) and associated unit
    """

    unit = kwargs.pop("unit", "mm")

    return Point_Array3(
        x=_np.linspace(start[0], end[0], num_points),
        y=_np.linspace(start[1], end[1], num_points),
        z=_np.linspace(start[2], end[2], num_points),
        unit=unit,
    )

plane3D(origin, point_a, point_b, num_points=100)

Generates a plane of points defined by

Parameters:

Name Type Description Default
origin tuple

Origin of plane

required
point_a tuple

Point defining end of vector in x-direction

required
point_b tuple

Point defining end of vector in y-direction

required
num_points int

Number of points in each direction, for a total of num_points^2. Defaults to 100.

100

Returns:

Type Description
Point_Array3

Struct containing the x,y,z coordinates

Source code in src/pymagnet/utils/_routines3D.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def plane3D(origin, point_a, point_b, num_points=100):
    """Generates a plane of points defined by

    Args:
        origin (tuple): Origin of plane
        point_a (tuple): Point defining end of vector in x-direction
        point_b (tuple): Point defining end of vector in y-direction
        num_points (int, optional): Number of points in each direction, for a total of num_points^2. Defaults to 100.

    Returns:
        Point_Array3: Struct containing the x,y,z coordinates
    """

    vector_a = point_a - origin
    vector_b = point_b - origin
    num_points_j = num_points * 1j
    x_elements, y_elements = _np.mgrid[0:1:num_points_j, 0:1:num_points_j]
    x = origin[0] + x_elements * vector_a[0] + y_elements * vector_b[0]
    y = origin[1] + x_elements * vector_a[1] + y_elements * vector_b[1]
    z = origin[2] + x_elements * vector_a[2] + y_elements * vector_b[2]

    return Point_Array3(x=x, y=y, z=z)

point3D(point, **kwargs)

Returns a single point

Parameters:

Name Type Description Default
point tuple

Coordinates (x, y, z) of the point.

required

Other Parameters:

Name Type Description
unit str

length scale units. Defaults to "mm".

Returns:

Type Description
Point_Array3

struct of x, y, and z values of shape (1) and associated unit

Source code in src/pymagnet/utils/_routines3D.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def point3D(point, **kwargs):
    """Returns a single point

    Args:
        point (tuple): Coordinates (x, y, z) of the point.

    Other Parameters:
        unit (str): length scale units. Defaults to "mm".

    Returns:
        Point_Array3: struct of x, y, and z values of shape (1) and associated unit
    """

    unit = kwargs.pop("unit", "mm")

    return Point_Array3(
        x=point[0],
        y=point[1],
        z=point[2],
        unit=unit,
    )

slice3D(plane='xy', max1=1.0, max2=1.0, slice_value=0.0, unit='mm', **kwargs)

Generates a planar slice of values

Parameters:

Name Type Description Default
plane str

plane. Defaults to "xy".

'xy'
max1 float

maximum along axis 1. Defaults to 1.0.

1.0
max2 float

maximum along axis 2. Defaults to 1.0.

1.0
slice_value float

constant value for third axis. Defaults to 0.0.

0.0
unit str

length scale units. Defaults to "mm".

'mm'
Kwargs

num_points (int): Number of points in each direction. Defaults to 100 min1 (float): minimum along axis 1. Defaults to -min1 min2 (float): minimum along axis 2. Defaults to -min2

Raises:

Type Description
Exception

plane type, must be one of 'xy', 'xz, 'yz', or 'custom'

Returns:

Type Description
Point_Array3

array of x, y, and z values of shape (num_points, num_points) and associated unit

Source code in src/pymagnet/utils/_routines3D.py
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
def slice3D(plane="xy", max1=1.0, max2=1.0, slice_value=0.0, unit="mm", **kwargs):
    """Generates a planar slice of values

    Args:
        plane (str, optional): plane. Defaults to "xy".
        max1 (float, optional): maximum along axis 1. Defaults to 1.0.
        max2 (float, optional): maximum along axis 2. Defaults to 1.0.
        slice_value (float, optional): constant value for third axis. Defaults to 0.0.
        unit (str, optional): length scale units. Defaults to "mm".

    Kwargs:
        num_points (int): Number of points in each direction. Defaults to 100
        min1 (float): minimum along axis 1. Defaults to -min1
        min2 (float): minimum along axis 2. Defaults to -min2

    Raises:
        Exception: plane type, must be one of 'xy', 'xz, 'yz', or 'custom'

    Returns:
        Point_Array3: array of x, y, and z values of shape (num_points, num_points) and associated unit
    """
    num_points = kwargs.pop("num_points", 100)
    min1 = kwargs.pop("min1", -1 * max1)
    min2 = kwargs.pop("min2", -1 * max2)
    NPj = num_points * 1j

    if plane.lower() == "xy":
        x, y = _np.mgrid[min1:max1:NPj, min2:max2:NPj]
        z = _np.asarray([slice_value])
        z = _np.tile(z, x.shape)

    elif plane.lower() == "xz":
        x, z = _np.mgrid[min1:max1:NPj, min2:max2:NPj]
        y = _np.asarray([slice_value])
        y = _np.tile(y, x.shape)

    elif plane.lower() == "yz":
        y, z = _np.mgrid[min1:max1:NPj, min2:max2:NPj]
        x = _np.asarray([slice_value])
        x = _np.tile(x, y.shape)

    elif plane.lower() == "custom":
        x = kwargs.pop("custom_x", _np.array([0.0]))
        y = kwargs.pop("custom_y", _np.array([0.0]))
        z = kwargs.pop("custom_z", _np.array([0.0]))

    else:
        raise ValueError("plane must be one of 'xy', 'xz, 'yz', or 'custom'")

    return Point_Array3(x, y, z, unit=unit)

Contains functions needed to rotate and translate a triangle to lie in the xz plane and to divide it into two right angled triangles

align_triangle_to_y(triangle, rot_axis, norm_vec)

Rotates and translates a triangle in lie in the xz plane

Parameters:

Name Type Description Default
triangle ndarray

vertices of a triangle

required
rot_axis ndarray

axis about which to rotate triangle

required
norm_vec ndarray

normal to triangle

required

Returns:

Type Description
tuple

aligned_triangle (ndarray, rotated triangle), first_rotation (quaternion, align to y-axis)

Source code in src/pymagnet/utils/_trigonometry3D.py
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
def align_triangle_to_y(triangle, rot_axis, norm_vec):
    """Rotates and translates a triangle in lie in the xz plane

    Args:
        triangle (ndarray): vertices of a triangle
        rot_axis (ndarray): axis about which to rotate triangle
        norm_vec (ndarray): normal to triangle

    Returns:
        tuple: aligned_triangle (ndarray, rotated triangle), first_rotation (quaternion, align to y-axis)
    """
    y_axis = _np.array([0, 1, 0])

    if _np.linalg.norm(rot_axis) < ALIGN_CUTOFF:
        # Check if parallel or anti-parallel
        if check_sign(y_axis, norm_vec):
            # Parallel
            first_rotation = Quaternion()
            aligned_triangle = triangle

        else:
            # Anti-parallel
            first_rotation = q_angle_from_axis(PI, y_axis)
            aligned_triangle = rotate_points(triangle, first_rotation)

    else:
        angle = -_safe_arccos(_np.dot(y_axis, norm_vec))
        first_rotation = q_angle_from_axis(angle, rot_axis)
        aligned_triangle = rotate_points(triangle, first_rotation)
    return aligned_triangle, first_rotation

align_triangle_to_y_njit(triangle, rot_axis, norm_vec)

Rotates a triangle to lie in the xz plane (numba-compatible).

Parameters:

Name Type Description Default
triangle ndarray

(3,3) vertices of a triangle

required
rot_axis ndarray

(3,) axis about which to rotate triangle

required
norm_vec ndarray

(3,) normal to triangle

required

Returns:

Type Description
tuple

(aligned_triangle, first_rotation_quat)

Source code in src/pymagnet/utils/_trigonometry3D.py
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
@njit(cache=True)
def align_triangle_to_y_njit(triangle, rot_axis, norm_vec):
    """Rotates a triangle to lie in the xz plane (numba-compatible).

    Args:
        triangle (ndarray): (3,3) vertices of a triangle
        rot_axis (ndarray): (3,) axis about which to rotate triangle
        norm_vec (ndarray): (3,) normal to triangle

    Returns:
        tuple: (aligned_triangle, first_rotation_quat)
    """
    y_axis = _np.array([0.0, 1.0, 0.0])

    rot_axis_norm = vec3_norm(rot_axis)

    if rot_axis_norm < ALIGN_CUTOFF:
        # Vectors are parallel or anti-parallel
        if vectors_same_direction(y_axis, norm_vec):
            # Parallel - no rotation needed
            first_rotation = quat_identity()
            aligned_triangle = triangle.copy()
        else:
            # Anti-parallel - rotate 180ยฐ about y
            first_rotation = quat_from_axis_angle(PI, y_axis)
            aligned_triangle = quat_rotate_points(first_rotation, triangle)
    else:
        # General case - rotate about cross product axis
        angle = -safe_arccos(vec3_dot(y_axis, norm_vec))
        first_rotation = quat_from_axis_angle(angle, rot_axis)
        aligned_triangle = quat_rotate_points(first_rotation, triangle)

    return aligned_triangle, first_rotation

align_triangle_xz(triangle, longest_side)

Returns quaternions needed to rotate a triangle lying in the xz plane to be aligned with its longest side along x and altitude along z

Parameters:

Name Type Description Default
triangle ndarray

vertices of triangle

required
longest_side int

index of the longest side of triangle

required

Returns:

Type Description
tuple

second_rotation (quaternion, align to x-axis), third_rotation (quaternion, align to z-axis):

Source code in src/pymagnet/utils/_trigonometry3D.py
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
def align_triangle_xz(triangle, longest_side):
    """Returns quaternions needed to rotate a triangle lying in the xz plane to be
    aligned with its longest side along x and altitude along z

    Args:
        triangle (ndarray): vertices of triangle
        longest_side (int): index of the longest side of triangle

    Returns:
        tuple: second_rotation (quaternion, align to x-axis), third_rotation (quaternion, align to z-axis):
    """
    x_axis = _np.array([1, 0, 0])
    y_axis = _np.array([0, 1, 0])
    z_axis = _np.array([0, 0, 1])

    # side_list = [0, 1, 2]
    # side_list.pop(longest_side)

    vec_x = return_axis_vector(triangle, longest_side)
    rot_axis = _np.cross(x_axis, vec_x)

    # Check aligment of base of triangle with x-axis
    if _np.linalg.norm(rot_axis) < ALIGN_CUTOFF:
        # Check if parallel or anti-parallel
        if check_sign(x_axis, vec_x):
            # Parallel
            second_rotation = Quaternion()
            tri_x = triangle
        else:
            # Anti-parallel
            second_rotation = q_angle_from_axis(PI, y_axis)
            tri_x = rotate_points(triangle, second_rotation)

    else:
        angle = -_safe_arccos(_np.dot(x_axis, vec_x))
        second_rotation = q_angle_from_axis(angle, rot_axis)
        tri_x = rotate_points(triangle, second_rotation)

    vec_z = return_z_vector(tri_x, longest_side)
    rot_axis = _np.cross(z_axis, vec_z)

    # Check aligment of triangle altitude with z-axis
    if _np.all(_np.fabs([rot_axis]) < ALIGN_CUTOFF):
        # Check if parallel anti-parallel
        if check_sign(z_axis, vec_z):
            # Parallel
            third_rotation = Quaternion()
        else:
            # Anti-parallel
            third_rotation = q_angle_from_axis(PI, y_axis)

    else:
        angle = -_safe_arccos(_np.dot(z_axis, vec_z))
        third_rotation = q_angle_from_axis(angle, rot_axis)

    return second_rotation, third_rotation

align_triangle_xz_njit(triangle, longest_side)

Aligns triangle with longest side along x and altitude along z.

Numba-compatible version.

Parameters:

Name Type Description Default
triangle ndarray

(3,3) vertices of triangle (should be in xz plane)

required
longest_side int

index of the longest side of triangle

required

Returns:

Type Description
tuple

(second_rotation_quat, third_rotation_quat)

Source code in src/pymagnet/utils/_trigonometry3D.py
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
636
637
638
639
640
641
642
643
644
645
646
647
@njit(cache=True)
def align_triangle_xz_njit(triangle, longest_side):
    """Aligns triangle with longest side along x and altitude along z.

    Numba-compatible version.

    Args:
        triangle (ndarray): (3,3) vertices of triangle (should be in xz plane)
        longest_side (int): index of the longest side of triangle

    Returns:
        tuple: (second_rotation_quat, third_rotation_quat)
    """
    x_axis = _np.array([1.0, 0.0, 0.0])
    y_axis = _np.array([0.0, 1.0, 0.0])
    z_axis = _np.array([0.0, 0.0, 1.0])

    # Get unit vector along longest side
    vec_x = return_axis_vector_njit(triangle, longest_side)
    rot_axis = vec3_cross(x_axis, vec_x)

    # Check alignment of base with x-axis
    if vec3_norm(rot_axis) < ALIGN_CUTOFF:
        if vectors_same_direction(x_axis, vec_x):
            # Parallel
            second_rotation = quat_identity()
            tri_x = triangle.copy()
        else:
            # Anti-parallel
            second_rotation = quat_from_axis_angle(PI, y_axis)
            tri_x = quat_rotate_points(second_rotation, triangle)
    else:
        angle = -safe_arccos(vec3_dot(x_axis, vec_x))
        second_rotation = quat_from_axis_angle(angle, rot_axis)
        tri_x = quat_rotate_points(second_rotation, triangle)

    # Get altitude direction and align to z
    vec_z = return_z_vector_njit(tri_x, longest_side)
    rot_axis = vec3_cross(z_axis, vec_z)

    # Check alignment of altitude with z-axis
    if vec3_norm(rot_axis) < ALIGN_CUTOFF:
        if vectors_same_direction(z_axis, vec_z):
            # Parallel
            third_rotation = quat_identity()
        else:
            # Anti-parallel
            third_rotation = quat_from_axis_angle(PI, y_axis)
    else:
        angle = -safe_arccos(vec3_dot(z_axis, vec_z))
        third_rotation = quat_from_axis_angle(angle, rot_axis)

    return second_rotation, third_rotation

altitude(a, b, c)

Gets altitude to side a of a triangle using Heron's formula.

Parameters:

Name Type Description Default
a float

base side (altitude is perpendicular to this)

required
b float

second side

required
c float

third side

required

Returns:

Type Description
float

altitude to side a

Raises:

Type Description
ValueError

If triangle is degenerate (sides violate triangle inequality)

Source code in src/pymagnet/utils/_trigonometry3D.py
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
def altitude(a, b, c):
    """Gets altitude to side `a` of a triangle using Heron's formula.

    Args:
        a (float): base side (altitude is perpendicular to this)
        b (float): second side
        c (float): third side

    Returns:
        float: altitude to side `a`

    Raises:
        ValueError: If triangle is degenerate (sides violate triangle inequality)
    """
    if a < 1e-10:
        raise ValueError("Degenerate triangle: base side has zero length")

    s = (a + b + c) / 2
    radicand = s * (s - a) * (s - b) * (s - c)

    if radicand < 0:
        raise ValueError(
            f"Degenerate triangle: sides ({a:.6f}, {b:.6f}, {c:.6f}) "
            "violate triangle inequality"
        )

    return 2 * _np.sqrt(radicand) / a

altitude_njit(a, b, c)

Gets altitude to side a using Heron's formula (numba-compatible).

Parameters:

Name Type Description Default
a float

base side (altitude is perpendicular to this)

required
b float

second side

required
c float

third side

required

Returns:

Type Description
float

altitude to side a, or 0 if degenerate

Source code in src/pymagnet/utils/_trigonometry3D.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
@njit(cache=True)
def altitude_njit(a, b, c):
    """Gets altitude to side `a` using Heron's formula (numba-compatible).

    Args:
        a (float): base side (altitude is perpendicular to this)
        b (float): second side
        c (float): third side

    Returns:
        float: altitude to side `a`, or 0 if degenerate
    """
    if a < 1e-10:
        return 0.0

    s = (a + b + c) / 2.0
    radicand = s * (s - a) * (s - b) * (s - c)

    if radicand < 0:
        return 0.0

    return 2.0 * _np.sqrt(radicand) / a

check_sign(vector_1, vector_2)

Returns true if the signs of all elements of two arrays are the same

Parameters:

Name Type Description Default
vector_1 ndarray

input array 2

required
vector_2 ndarray

input array 2

required

Returns:

Type Description
boolean

True if elements in two arrays have the same sign

Source code in src/pymagnet/utils/_trigonometry3D.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def check_sign(vector_1, vector_2):
    """Returns true if the signs of all elements of two arrays are the same

    Args:
        vector_1 (ndarray): input array 2
        vector_2 (ndarray): input array 2

    Returns:
        boolean: True if elements in two arrays have the same sign
    """
    sign_comp_1 = _np.fabs(vector_1 + vector_2)
    sign_comp_2 = _np.fabs(vector_1) + _np.fabs(vector_2)

    return _np.allclose(sign_comp_1, sign_comp_2, atol=1e-6)

norm_plane(vec)

Calculates the normal to a triangular plane.

Parameters:

Name Type Description Default
vec ndarray

(3,3) array of triangle vertices

required

Returns:

Type Description
ndarray

unit normal vector (3,)

Raises:

Type Description
ValueError

If triangle is degenerate (collinear vertices)

Source code in src/pymagnet/utils/_trigonometry3D.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def norm_plane(vec):
    """Calculates the normal to a triangular plane.

    Args:
        vec (ndarray): (3,3) array of triangle vertices

    Returns:
        ndarray: unit normal vector (3,)

    Raises:
        ValueError: If triangle is degenerate (collinear vertices)
    """
    norm = _np.cross(vec[1] - vec[0], vec[2] - vec[0])
    length = _np.linalg.norm(norm)

    if length < 1e-10:
        raise ValueError("Degenerate triangle: vertices are collinear")

    return norm / length

norm_plane_njit(vec)

Calculates the normal to a triangular plane (numba-compatible).

Parameters:

Name Type Description Default
vec ndarray

(3,3) array of triangle vertices

required

Returns:

Type Description
ndarray

unit normal vector (3,), or zero vector if degenerate

Source code in src/pymagnet/utils/_trigonometry3D.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
@njit(cache=True)
def norm_plane_njit(vec):
    """Calculates the normal to a triangular plane (numba-compatible).

    Args:
        vec (ndarray): (3,3) array of triangle vertices

    Returns:
        ndarray: unit normal vector (3,), or zero vector if degenerate
    """
    # Cross product of two edges
    e1 = vec[1] - vec[0]
    e2 = vec[2] - vec[0]
    norm = vec3_cross(e1, e2)
    length = vec3_norm(norm)

    if length < 1e-10:
        # Return zero vector for degenerate triangle
        return _np.array([0.0, 0.0, 0.0])

    return norm / length

return_axis_vector(triangle, longest_side)

Returns unit vector along the longest side of a triangle.

Parameters:

Name Type Description Default
triangle ndarray

(3,3) array of triangle vertices

required
longest_side int

index of longest edge (0, 1, or 2)

required

Returns:

Type Description
ndarray

unit vector (3,) along longest edge

Source code in src/pymagnet/utils/_trigonometry3D.py
208
209
210
211
212
213
214
215
216
217
218
219
220
def return_axis_vector(triangle, longest_side):
    """Returns unit vector along the longest side of a triangle.

    Args:
        triangle (ndarray): (3,3) array of triangle vertices
        longest_side (int): index of longest edge (0, 1, or 2)

    Returns:
        ndarray: unit vector (3,) along longest edge
    """
    i, j = _EDGE_VERTICES[longest_side]
    vec = triangle[j] - triangle[i]
    return vec / _np.linalg.norm(vec)

return_axis_vector_njit(triangle, longest_side)

Returns unit vector along the longest side of a triangle (numba-compatible).

Parameters:

Name Type Description Default
triangle ndarray

(3,3) array of triangle vertices

required
longest_side int

index of longest edge (0, 1, or 2)

required

Returns:

Type Description
ndarray

unit vector (3,) along longest edge

Source code in src/pymagnet/utils/_trigonometry3D.py
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
@njit(cache=True)
def return_axis_vector_njit(triangle, longest_side):
    """Returns unit vector along the longest side of a triangle (numba-compatible).

    Args:
        triangle (ndarray): (3,3) array of triangle vertices
        longest_side (int): index of longest edge (0, 1, or 2)

    Returns:
        ndarray: unit vector (3,) along longest edge
    """
    if longest_side == 0:
        vec = triangle[1] - triangle[0]
    elif longest_side == 1:
        vec = triangle[2] - triangle[1]
    else:
        vec = triangle[2] - triangle[0]

    return vec3_normalize(vec)

return_z_vector(triangle, longest_side)

Returns unit altitude vector from longest side toward opposite vertex.

For a triangle aligned in the xz plane with longest side along x, this returns the direction toward the apex (along z).

Parameters:

Name Type Description Default
triangle ndarray

(3,3) array of triangle vertices

required
longest_side int

index of longest edge (0, 1, or 2)

required

Returns:

Type Description
ndarray

unit altitude vector (3,)

Raises:

Type Description
ValueError

If altitude has zero length (degenerate triangle)

Source code in src/pymagnet/utils/_trigonometry3D.py
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
def return_z_vector(triangle, longest_side):
    """Returns unit altitude vector from longest side toward opposite vertex.

    For a triangle aligned in the xz plane with longest side along x,
    this returns the direction toward the apex (along z).

    Args:
        triangle (ndarray): (3,3) array of triangle vertices
        longest_side (int): index of longest edge (0, 1, or 2)

    Returns:
        ndarray: unit altitude vector (3,)

    Raises:
        ValueError: If altitude has zero length (degenerate triangle)
    """
    # Get the vertex opposite to the longest side
    opposite_idx = _OPPOSITE_VERTEX[longest_side]

    # Get endpoints of the longest side
    start_idx, end_idx = _EDGE_VERTICES[longest_side]

    # Project opposite vertex onto the line of longest side
    edge_vec = triangle[end_idx] - triangle[start_idx]
    edge_unit = edge_vec / _np.linalg.norm(edge_vec)

    to_opposite = triangle[opposite_idx] - triangle[start_idx]
    projection_length = _np.dot(to_opposite, edge_unit)
    foot_of_altitude = triangle[start_idx] + projection_length * edge_unit

    # Altitude vector: from foot to opposite vertex
    altitude_vec = triangle[opposite_idx] - foot_of_altitude
    length = _np.linalg.norm(altitude_vec)

    if length < 1e-10:
        raise ValueError("Degenerate triangle: altitude has zero length")

    return altitude_vec / length

return_z_vector_njit(triangle, longest_side)

Returns unit altitude vector from longest side toward opposite vertex.

Numba-compatible version.

Parameters:

Name Type Description Default
triangle ndarray

(3,3) array of triangle vertices

required
longest_side int

index of longest edge (0, 1, or 2)

required

Returns:

Type Description
ndarray

unit altitude vector (3,)

Source code in src/pymagnet/utils/_trigonometry3D.py
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
@njit(cache=True)
def return_z_vector_njit(triangle, longest_side):
    """Returns unit altitude vector from longest side toward opposite vertex.

    Numba-compatible version.

    Args:
        triangle (ndarray): (3,3) array of triangle vertices
        longest_side (int): index of longest edge (0, 1, or 2)

    Returns:
        ndarray: unit altitude vector (3,)
    """
    # Get edge endpoints and opposite vertex based on longest_side
    if longest_side == 0:
        start = triangle[0]
        end = triangle[1]
        opposite = triangle[2]
    elif longest_side == 1:
        start = triangle[1]
        end = triangle[2]
        opposite = triangle[0]
    else:
        start = triangle[0]
        end = triangle[2]
        opposite = triangle[1]

    # Project opposite vertex onto the line of longest side
    edge_vec = end - start
    edge_unit = vec3_normalize(edge_vec)

    to_opposite = opposite - start
    projection_length = vec3_dot(to_opposite, edge_unit)
    foot_of_altitude = start + projection_length * edge_unit

    # Altitude vector: from foot to opposite vertex
    altitude_vec = opposite - foot_of_altitude

    return vec3_normalize(altitude_vec)

rotate_points(points, rotation_quaternion)

Rotates a set of points

Parameters:

Name Type Description Default
points [type]

[description]

required
rotation_quaternion [type]

[description]

required

Returns:

Type Description
[type]

[description]

Source code in src/pymagnet/utils/_trigonometry3D.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def rotate_points(points, rotation_quaternion):
    """Rotates a set of points

    Args:
        points ([type]): [description]
        rotation_quaternion ([type]): [description]

    Returns:
        [type]: [description]
    """
    x_rot, y_rot, z_rot = rotation_quaternion * points.T
    rotate_points = _np.vstack([x_rot, y_rot, z_rot]).T

    return rotate_points

rotate_vector_by_quat_inverse_njit(q, x, y, z)

Rotate coordinate arrays by inverse of quaternion.

Numba-compatible function for inverse rotation.

Parameters:

Name Type Description Default
q ndarray

(4,) quaternion [w, x, y, z]

required
x ndarray

x coordinates (flattened)

required
y ndarray

y coordinates (flattened)

required
z ndarray

z coordinates (flattened)

required

Returns:

Type Description
tuple

(x_rot, y_rot, z_rot) rotated coordinates

Source code in src/pymagnet/utils/_trigonometry3D.py
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
@njit(cache=True)
def rotate_vector_by_quat_inverse_njit(q, x, y, z):
    """Rotate coordinate arrays by inverse of quaternion.

    Numba-compatible function for inverse rotation.

    Args:
        q (ndarray): (4,) quaternion [w, x, y, z]
        x (ndarray): x coordinates (flattened)
        y (ndarray): y coordinates (flattened)
        z (ndarray): z coordinates (flattened)

    Returns:
        tuple: (x_rot, y_rot, z_rot) rotated coordinates
    """
    n = x.size
    x_rot = _np.empty(n)
    y_rot = _np.empty(n)
    z_rot = _np.empty(n)

    # Conjugate for inverse
    qw, qx, qy, qz = q[0], -q[1], -q[2], -q[3]

    for i in range(n):
        vx, vy, vz = x[i], y[i], z[i]

        tx = 2.0 * (qy * vz - qz * vy)
        ty = 2.0 * (qz * vx - qx * vz)
        tz = 2.0 * (qx * vy - qy * vx)

        x_rot[i] = vx + qw * tx + (qy * tz - qz * ty)
        y_rot[i] = vy + qw * ty + (qz * tx - qx * tz)
        z_rot[i] = vz + qw * tz + (qx * ty - qy * tx)

    return x_rot, y_rot, z_rot

rotate_vector_by_quat_njit(q, x, y, z)

Rotate coordinate arrays by quaternion.

Numba-compatible function to rotate arrays of coordinates.

Parameters:

Name Type Description Default
q ndarray

(4,) quaternion [w, x, y, z]

required
x ndarray

x coordinates (flattened)

required
y ndarray

y coordinates (flattened)

required
z ndarray

z coordinates (flattened)

required

Returns:

Type Description
tuple

(x_rot, y_rot, z_rot) rotated coordinates

Source code in src/pymagnet/utils/_trigonometry3D.py
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
@njit(cache=True)
def rotate_vector_by_quat_njit(q, x, y, z):
    """Rotate coordinate arrays by quaternion.

    Numba-compatible function to rotate arrays of coordinates.

    Args:
        q (ndarray): (4,) quaternion [w, x, y, z]
        x (ndarray): x coordinates (flattened)
        y (ndarray): y coordinates (flattened)
        z (ndarray): z coordinates (flattened)

    Returns:
        tuple: (x_rot, y_rot, z_rot) rotated coordinates
    """
    n = x.size
    x_rot = _np.empty(n)
    y_rot = _np.empty(n)
    z_rot = _np.empty(n)

    qw, qx, qy, qz = q[0], q[1], q[2], q[3]

    for i in range(n):
        vx, vy, vz = x[i], y[i], z[i]

        # t = 2 * cross(q.xyz, v)
        tx = 2.0 * (qy * vz - qz * vy)
        ty = 2.0 * (qz * vx - qx * vz)
        tz = 2.0 * (qx * vy - qy * vx)

        # result = v + w*t + cross(q.xyz, t)
        x_rot[i] = vx + qw * tx + (qy * tz - qz * ty)
        y_rot[i] = vy + qw * ty + (qz * tx - qx * tz)
        z_rot[i] = vz + qw * tz + (qx * ty - qy * tx)

    return x_rot, y_rot, z_rot

signed_area(triangle)

Calculates signed area of a triangle. Area area < 0 for clockwise ordering. Assumes the triangle is in the xz plane (i.e. with the normal parallel to y).

Parameters:

Name Type Description Default
triangle ndarray

3x3 array of vertices

required

Returns:

Type Description
float

signed area

Source code in src/pymagnet/utils/_trigonometry3D.py
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
@jit
def signed_area(triangle):
    """Calculates signed area of a triangle. Area area < 0 for clockwise ordering.
    Assumes the triangle is in the xz plane (i.e. with the normal parallel to y).

    Args:
        triangle (ndarray): 3x3 array of vertices

    Returns:
        float: signed area
    """

    j = 1
    NP = 3
    area = 0.0

    for i in range(NP):
        j = j % NP

        area += (triangle[j][0] - triangle[i][0]) * (triangle[j][2] + triangle[i][2])
        j += 1

    # check winding order of polygon, area < 0 for clockwise ordering of points
    area /= 2.0

    return area

pymagnets.utils._vector_structs

Private module consiting of vector and point array classes and their methods.

Field1

Bases: Point_Array1

1D Field vector This is used to contain one component (Bz as z), and the units ('T', 'mT', etc)

Source code in src/pymagnet/utils/_vector_structs.py
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
class Field1(Point_Array1):
    """1D Field vector
    This is used to contain one component (Bz as z), and the units
    ('T', 'mT', etc)
    """

    def __init__(self, z, unit="T"):
        """Init method

        Args:
            z (ndarray): Magnetic field component
            unit (str, optional): Unit of field. Defaults to "T".

        Raises:
            ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
        """
        super().__init__(z)
        if get_unit_value_tesla(unit) is not None:
            self.unit = unit
        else:
            raise ValueError("Error, not an SI prefix, e.g. T, mT, uT, nT ")

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nBz: {self.z}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nBz: {self.z}]"

    def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
        """Converts field array to a different unit. e.g from 'T' to 'mT'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
        """
        super().change_unit(new_unit, get_unit_value)

__init__(z, unit='T')

Init method

Parameters:

Name Type Description Default
z ndarray

Magnetic field component

required
unit str

Unit of field. Defaults to "T".

'T'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. T, mT, uT, nT

Source code in src/pymagnet/utils/_vector_structs.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
def __init__(self, z, unit="T"):
    """Init method

    Args:
        z (ndarray): Magnetic field component
        unit (str, optional): Unit of field. Defaults to "T".

    Raises:
        ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
    """
    super().__init__(z)
    if get_unit_value_tesla(unit) is not None:
        self.unit = unit
    else:
        raise ValueError("Error, not an SI prefix, e.g. T, mT, uT, nT ")

change_unit(new_unit, get_unit_value=get_unit_value_tesla)

Converts field array to a different unit. e.g from 'T' to 'mT'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_tesla.

get_unit_value_tesla
Source code in src/pymagnet/utils/_vector_structs.py
283
284
285
286
287
288
289
290
def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
    """Converts field array to a different unit. e.g from 'T' to 'mT'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
    """
    super().change_unit(new_unit, get_unit_value)

Field2

Bases: Point_Array2

2D Field vector This is used to contain two components (x, y), and the units ('T', 'mT', etc)

Source code in src/pymagnet/utils/_vector_structs.py
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
class Field2(Point_Array2):
    """2D Field vector
    This is used to contain two components (x, y), and the units
    ('T', 'mT', etc)
    """

    def __init__(self, x, y, unit="T"):
        """Init method

        Args:
            x (ndarray): Magnetic field component Bx
            y (ndarray): Magnetic field component By
            unit (str, optional): Unit of field. Defaults to "T".

        Raises:
            ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
        """
        super().__init__(x, y)
        self.n = _np.zeros_like(x)
        if get_unit_value_tesla(unit) is not None:
            self.unit = unit
        else:
            raise ValueError("Error, not an SI prefix, e.g. T, mT, uT, nT ")

    def calc_norm(self):
        """Calculates the norm of the 2D vector"""
        self.n = _np.linalg.norm([self.x, self.y], axis=0)

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nBx: {self.x}\nBy: {self.y}\nBn: {self.n}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nBx: {self.x}\nBy: {self.y}\nBn: {self.n}]"

    def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
        """Converts field array to a different unit. e.g from 'T' to 'mT'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
        """
        super().change_unit(new_unit, get_unit_value)

__init__(x, y, unit='T')

Init method

Parameters:

Name Type Description Default
x ndarray

Magnetic field component Bx

required
y ndarray

Magnetic field component By

required
unit str

Unit of field. Defaults to "T".

'T'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. T, mT, uT, nT

Source code in src/pymagnet/utils/_vector_structs.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def __init__(self, x, y, unit="T"):
    """Init method

    Args:
        x (ndarray): Magnetic field component Bx
        y (ndarray): Magnetic field component By
        unit (str, optional): Unit of field. Defaults to "T".

    Raises:
        ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
    """
    super().__init__(x, y)
    self.n = _np.zeros_like(x)
    if get_unit_value_tesla(unit) is not None:
        self.unit = unit
    else:
        raise ValueError("Error, not an SI prefix, e.g. T, mT, uT, nT ")

calc_norm()

Calculates the norm of the 2D vector

Source code in src/pymagnet/utils/_vector_structs.py
317
318
319
def calc_norm(self):
    """Calculates the norm of the 2D vector"""
    self.n = _np.linalg.norm([self.x, self.y], axis=0)

change_unit(new_unit, get_unit_value=get_unit_value_tesla)

Converts field array to a different unit. e.g from 'T' to 'mT'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_tesla.

get_unit_value_tesla
Source code in src/pymagnet/utils/_vector_structs.py
327
328
329
330
331
332
333
334
def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
    """Converts field array to a different unit. e.g from 'T' to 'mT'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
    """
    super().change_unit(new_unit, get_unit_value)

Field3

Bases: Point_Array3

3D Field vector This is used to contain three components (x, y), and the units ('T', 'mT', etc)

Source code in src/pymagnet/utils/_vector_structs.py
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
class Field3(Point_Array3):
    """3D Field vector
    This is used to contain three components (x, y), and the units
    ('T', 'mT', etc)
    """

    def __init__(self, x, y, z, unit="T"):
        """Init method

        Args:
            x (ndarray): Magnetic field component Bx
            y (ndarray): Magnetic field component By
            z (ndarray): Magnetic field component Bz
            unit (str, optional): Unit of field. Defaults to "T".

        Raises:
            ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
        """
        super().__init__(x, y, z)
        self.n = _np.zeros_like(x)
        self.unit = unit

    def calc_norm(self):
        """Calculates the norm of the 3D vector"""
        self.n = _np.linalg.norm([self.x, self.y, self.z], axis=0)

    def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
        """Converts field array to a different unit. e.g from 'T' to 'mT'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
        """
        super().change_unit(new_unit, get_unit_value)

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nBx: {self.x}\nBy: {self.y}\nBz: {self.z}\nBn: {self.n}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nBx: {self.x}\nBy: {self.y}\nBz: {self.z}\nBn: {self.n}]"

__init__(x, y, z, unit='T')

Init method

Parameters:

Name Type Description Default
x ndarray

Magnetic field component Bx

required
y ndarray

Magnetic field component By

required
z ndarray

Magnetic field component Bz

required
unit str

Unit of field. Defaults to "T".

'T'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. T, mT, uT, nT

Source code in src/pymagnet/utils/_vector_structs.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
def __init__(self, x, y, z, unit="T"):
    """Init method

    Args:
        x (ndarray): Magnetic field component Bx
        y (ndarray): Magnetic field component By
        z (ndarray): Magnetic field component Bz
        unit (str, optional): Unit of field. Defaults to "T".

    Raises:
        ValueError: Unit must an SI prefix, e.g. T, mT, uT, nT
    """
    super().__init__(x, y, z)
    self.n = _np.zeros_like(x)
    self.unit = unit

calc_norm()

Calculates the norm of the 3D vector

Source code in src/pymagnet/utils/_vector_structs.py
359
360
361
def calc_norm(self):
    """Calculates the norm of the 3D vector"""
    self.n = _np.linalg.norm([self.x, self.y, self.z], axis=0)

change_unit(new_unit, get_unit_value=get_unit_value_tesla)

Converts field array to a different unit. e.g from 'T' to 'mT'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_tesla.

get_unit_value_tesla
Source code in src/pymagnet/utils/_vector_structs.py
363
364
365
366
367
368
369
370
def change_unit(self, new_unit, get_unit_value=get_unit_value_tesla):
    """Converts field array to a different unit. e.g from 'T' to 'mT'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_tesla.
    """
    super().change_unit(new_unit, get_unit_value)

Jacobian2 dataclass

2D Jacobian tensor of the magnetic field: J_ij = dB_i/dx_j.

Attributes:

Name Type Description
dBx_dx ndarray

Partial derivative of Bx with respect to x.

dBx_dy ndarray

Partial derivative of Bx with respect to y.

dBy_dx ndarray

Partial derivative of By with respect to x.

dBy_dy ndarray

Partial derivative of By with respect to y.

Source code in src/pymagnet/utils/_vector_structs.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
@dataclass
class Jacobian2:
    """2D Jacobian tensor of the magnetic field: J_ij = dB_i/dx_j.

    Attributes:
        dBx_dx: Partial derivative of Bx with respect to x.
        dBx_dy: Partial derivative of Bx with respect to y.
        dBy_dx: Partial derivative of By with respect to x.
        dBy_dy: Partial derivative of By with respect to y.
    """

    dBx_dx: _np.ndarray
    dBx_dy: _np.ndarray
    dBy_dx: _np.ndarray
    dBy_dy: _np.ndarray

Jacobian3 dataclass

3D Jacobian tensor of the magnetic field: J_ij = dB_i/dx_j.

Attributes:

Name Type Description
dBx_dx ndarray

Partial derivative of Bx with respect to x.

dBx_dy ndarray

Partial derivative of Bx with respect to y.

dBx_dz ndarray

Partial derivative of Bx with respect to z.

dBy_dx ndarray

Partial derivative of By with respect to x.

dBy_dy ndarray

Partial derivative of By with respect to y.

dBy_dz ndarray

Partial derivative of By with respect to z.

dBz_dx ndarray

Partial derivative of Bz with respect to x.

dBz_dy ndarray

Partial derivative of Bz with respect to y.

dBz_dz ndarray

Partial derivative of Bz with respect to z.

Source code in src/pymagnet/utils/_vector_structs.py
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
@dataclass
class Jacobian3:
    """3D Jacobian tensor of the magnetic field: J_ij = dB_i/dx_j.

    Attributes:
        dBx_dx: Partial derivative of Bx with respect to x.
        dBx_dy: Partial derivative of Bx with respect to y.
        dBx_dz: Partial derivative of Bx with respect to z.
        dBy_dx: Partial derivative of By with respect to x.
        dBy_dy: Partial derivative of By with respect to y.
        dBy_dz: Partial derivative of By with respect to z.
        dBz_dx: Partial derivative of Bz with respect to x.
        dBz_dy: Partial derivative of Bz with respect to y.
        dBz_dz: Partial derivative of Bz with respect to z.
    """

    dBx_dx: _np.ndarray
    dBx_dy: _np.ndarray
    dBx_dz: _np.ndarray
    dBy_dx: _np.ndarray
    dBy_dy: _np.ndarray
    dBy_dz: _np.ndarray
    dBz_dx: _np.ndarray
    dBz_dy: _np.ndarray
    dBz_dz: _np.ndarray

Point_Array1

1D point structure This is used to contain one position array (z), and the units ('mm', 'cm', etc)

Source code in src/pymagnet/utils/_vector_structs.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
class Point_Array1:
    """1D point structure
    This is used to contain one position array (z), and the units
    ('mm', 'cm', etc)
    """

    def __init__(self, z, unit="mm"):
        """Init method

        Args:
            z (ndarray): z coordinates
            unit (str, optional): Unit of length. Defaults to "mm".

        Raises:
            ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
        """
        self.z = _np.asarray(z)
        if get_unit_value_meter(unit) is not None:
            self.unit = unit
        else:
            raise ValueError("Error, not an SI prefix, e.g. km, m, cm, mm ")

    def __repr__(self) -> str:
        return f"[(Unit:{self.unit}) Array:{self.z}]"

    def __str__(self) -> str:
        return f"[(Unit:{self.unit}) Array:{self.z}]"

    def get_unit(self):
        """Gets unit

        Returns:
            str: unit
        """
        return self.unit

    def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
        """Converts point array to a different unit. e.g from 'cm' to 'mm'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
        """
        from ..magnets import Cylinder, Magnet, Prism

        current_unit_val = get_unit_value(self.get_unit())
        new_unit_val = get_unit_value(new_unit)
        if current_unit_val is None or new_unit_val is None:
            raise ValueError(
                f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
                "unrecognised unit."
            )
        scale_val = current_unit_val / new_unit_val

        self.z *= scale_val

        self.unit = new_unit
        for magnet in Magnet.instances:
            magnet.center = scale_val * magnet.center
            if issubclass(magnet.__class__, Prism):
                magnet.a = magnet.a * scale_val
                magnet.b = magnet.b * scale_val
                magnet.c = magnet.c * scale_val
                magnet.width = magnet.width * scale_val
                magnet.depth = magnet.depth * scale_val
                magnet.height = magnet.height * scale_val
            elif issubclass(magnet.__class__, Cylinder):
                magnet.radius = magnet.radius * scale_val
                magnet.length = magnet.length * scale_val

__init__(z, unit='mm')

Init method

Parameters:

Name Type Description Default
z ndarray

z coordinates

required
unit str

Unit of length. Defaults to "mm".

'mm'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. km, m, cm, mm

Source code in src/pymagnet/utils/_vector_structs.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def __init__(self, z, unit="mm"):
    """Init method

    Args:
        z (ndarray): z coordinates
        unit (str, optional): Unit of length. Defaults to "mm".

    Raises:
        ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
    """
    self.z = _np.asarray(z)
    if get_unit_value_meter(unit) is not None:
        self.unit = unit
    else:
        raise ValueError("Error, not an SI prefix, e.g. km, m, cm, mm ")

change_unit(new_unit, get_unit_value=get_unit_value_meter)

Converts point array to a different unit. e.g from 'cm' to 'mm'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_meter.

get_unit_value_meter
Source code in src/pymagnet/utils/_vector_structs.py
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 change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
    """Converts point array to a different unit. e.g from 'cm' to 'mm'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
    """
    from ..magnets import Cylinder, Magnet, Prism

    current_unit_val = get_unit_value(self.get_unit())
    new_unit_val = get_unit_value(new_unit)
    if current_unit_val is None or new_unit_val is None:
        raise ValueError(
            f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
            "unrecognised unit."
        )
    scale_val = current_unit_val / new_unit_val

    self.z *= scale_val

    self.unit = new_unit
    for magnet in Magnet.instances:
        magnet.center = scale_val * magnet.center
        if issubclass(magnet.__class__, Prism):
            magnet.a = magnet.a * scale_val
            magnet.b = magnet.b * scale_val
            magnet.c = magnet.c * scale_val
            magnet.width = magnet.width * scale_val
            magnet.depth = magnet.depth * scale_val
            magnet.height = magnet.height * scale_val
        elif issubclass(magnet.__class__, Cylinder):
            magnet.radius = magnet.radius * scale_val
            magnet.length = magnet.length * scale_val

get_unit()

Gets unit

Returns:

Type Description
str

unit

Source code in src/pymagnet/utils/_vector_structs.py
47
48
49
50
51
52
53
def get_unit(self):
    """Gets unit

    Returns:
        str: unit
    """
    return self.unit

Point_Array2

2D point structure This is used to contain two position arrays (x, y), and the units ('mm', 'cm', etc)

Source code in src/pymagnet/utils/_vector_structs.py
 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
class Point_Array2:
    """2D point structure
    This is used to contain two position arrays (x, y), and the units
    ('mm', 'cm', etc)
    """

    def __init__(self, x, y, unit="mm"):
        """Init Method

        Args:
            x (ndarray): x coordinates
            y (ndarray): y coordinates
            unit (str, optional): Unit of length. Defaults to "mm".

        Raises:
            ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
        """
        self.x = _np.asarray(x)
        self.y = _np.asarray(y)
        if get_unit_value_meter(unit) is not None:
            self.unit = unit
        else:
            raise ValueError("Error, not an SI prefix, e.g. km, m, cm, mm ")

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nx: {self.x}\ny: {self.y}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nx: {self.x}\ny: {self.y}]"

    def get_unit(self):
        return self.unit

    def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
        """Converts point array to a different unit. e.g from 'cm' to 'mm'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
        """
        from ..magnets import Circle, Magnet, PolyMagnet, Rectangle, Square

        current_unit_val = get_unit_value(self.get_unit())
        new_unit_val = get_unit_value(new_unit)
        if current_unit_val is None or new_unit_val is None:
            raise ValueError(
                f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
                "unrecognised unit."
            )
        scale_val = current_unit_val / new_unit_val

        self.x *= scale_val
        self.y *= scale_val
        self.unit = new_unit
        for magnet in Magnet.instances:
            magnet.center = scale_val * magnet.center
            if issubclass(magnet.__class__, Rectangle):
                magnet.a = magnet.a * scale_val
                magnet.b = magnet.b * scale_val
                magnet.width = magnet.width * scale_val
                magnet.height = magnet.height * scale_val
            elif issubclass(magnet.__class__, Square):
                magnet.a = magnet.a * scale_val
                magnet.width = magnet.width * scale_val
            elif issubclass(magnet.__class__, Circle):
                magnet.radius = magnet.radius * scale_val
            elif issubclass(magnet.__class__, PolyMagnet):
                magnet.polygon.vertices = (
                    scale_val * _np.array(magnet.polygon.vertices)
                ).tolist()

__init__(x, y, unit='mm')

Init Method

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required
unit str

Unit of length. Defaults to "mm".

'mm'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. km, m, cm, mm

Source code in src/pymagnet/utils/_vector_structs.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def __init__(self, x, y, unit="mm"):
    """Init Method

    Args:
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        unit (str, optional): Unit of length. Defaults to "mm".

    Raises:
        ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
    """
    self.x = _np.asarray(x)
    self.y = _np.asarray(y)
    if get_unit_value_meter(unit) is not None:
        self.unit = unit
    else:
        raise ValueError("Error, not an SI prefix, e.g. km, m, cm, mm ")

change_unit(new_unit, get_unit_value=get_unit_value_meter)

Converts point array to a different unit. e.g from 'cm' to 'mm'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_meter.

get_unit_value_meter
Source code in src/pymagnet/utils/_vector_structs.py
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
def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
    """Converts point array to a different unit. e.g from 'cm' to 'mm'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
    """
    from ..magnets import Circle, Magnet, PolyMagnet, Rectangle, Square

    current_unit_val = get_unit_value(self.get_unit())
    new_unit_val = get_unit_value(new_unit)
    if current_unit_val is None or new_unit_val is None:
        raise ValueError(
            f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
            "unrecognised unit."
        )
    scale_val = current_unit_val / new_unit_val

    self.x *= scale_val
    self.y *= scale_val
    self.unit = new_unit
    for magnet in Magnet.instances:
        magnet.center = scale_val * magnet.center
        if issubclass(magnet.__class__, Rectangle):
            magnet.a = magnet.a * scale_val
            magnet.b = magnet.b * scale_val
            magnet.width = magnet.width * scale_val
            magnet.height = magnet.height * scale_val
        elif issubclass(magnet.__class__, Square):
            magnet.a = magnet.a * scale_val
            magnet.width = magnet.width * scale_val
        elif issubclass(magnet.__class__, Circle):
            magnet.radius = magnet.radius * scale_val
        elif issubclass(magnet.__class__, PolyMagnet):
            magnet.polygon.vertices = (
                scale_val * _np.array(magnet.polygon.vertices)
            ).tolist()

Point_Array3

Bases: Point_Array2

3D point structure This is used to contain three position arrays (x, y, z), and the units ('mm', 'cm', etc)

Source code in src/pymagnet/utils/_vector_structs.py
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
class Point_Array3(Point_Array2):
    """3D point structure
    This is used to contain three position arrays (x, y, z), and the units
    ('mm', 'cm', etc)
    """

    def __init__(self, x, y, z, unit="mm"):
        """Init Method

        Args:
            x (ndarray): x coordinates
            y (ndarray): y coordinates
            z (ndarray): z coordinates
            unit (str, optional): Unit of length. Defaults to "mm".

        Raises:
            ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
        """
        super().__init__(x, y, unit=unit)
        self.z = _np.asarray(z)

    def __repr__(self) -> str:
        return f"[Unit: {self.unit}\nx: {self.x}\ny: {self.y}\nz: {self.z}]"

    def __str__(self) -> str:
        return f"[Unit: {self.unit}\nx: {self.x}\ny: {self.y}\nz: {self.z}]"

    def rotate(self, alpha, beta, gamma):
        """Rotates set of Point_Array3 points by angles alpha, beta, gamma

        Args:
            alpha (float): Angle to rotate about z in degrees
            beta (float): Angle to rotate about y in degrees
            gamma (float): Angle to rotate about x in degrees

        Returns:
            Point_Array3: rotated set of points
        """
        q_fwd = Quaternion.gen_rotation_quaternion(
            _np.deg2rad(alpha), _np.deg2rad(beta), _np.deg2rad(gamma)
        )

        pos_vec = Quaternion._prepare_vector(self.x, self.y, self.z)
        x_rot, y_rot, z_rot = q_fwd * pos_vec
        return Point_Array3(
            x=x_rot.reshape(self.x.shape),
            y=y_rot.reshape(self.x.shape),
            z=z_rot.reshape(self.x.shape),
        )

    def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
        """Converts point array to a different unit. e.g from 'cm' to 'mm'

        Args:
            new_unit (str): unit to be converted to
            get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
        """
        from ..magnets import Cube, Cylinder, Magnet, Mesh, Prism, Sphere

        current_unit_val = get_unit_value(self.get_unit())
        new_unit_val = get_unit_value(new_unit)
        if current_unit_val is None or new_unit_val is None:
            raise ValueError(
                f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
                "unrecognised unit."
            )
        scale_val = current_unit_val / new_unit_val
        self.x *= scale_val
        self.y *= scale_val
        self.z *= scale_val
        self.unit = new_unit

        for magnet in Magnet.instances:
            magnet.center = scale_val * magnet.center
            if issubclass(magnet.__class__, Prism):
                magnet.a = magnet.a * scale_val
                magnet.b = magnet.b * scale_val
                magnet.c = magnet.c * scale_val
                magnet.width = magnet.width * scale_val
                magnet.height = magnet.height * scale_val
                magnet.depth = magnet.depth * scale_val
            elif issubclass(magnet.__class__, Cube):
                magnet.a = magnet.a * scale_val
                magnet.width = magnet.width * scale_val
            elif issubclass(magnet.__class__, Cylinder):
                magnet.radius = magnet.radius * scale_val
                magnet.length = magnet.length * scale_val
            elif issubclass(magnet.__class__, Sphere):
                magnet.radius = magnet.radius * scale_val
            elif issubclass(magnet.__class__, Mesh):
                magnet.mesh_vectors = magnet.mesh_vectors * scale_val

__init__(x, y, z, unit='mm')

Init Method

Parameters:

Name Type Description Default
x ndarray

x coordinates

required
y ndarray

y coordinates

required
z ndarray

z coordinates

required
unit str

Unit of length. Defaults to "mm".

'mm'

Raises:

Type Description
ValueError

Unit must an SI prefix, e.g. km, m, cm, mm

Source code in src/pymagnet/utils/_vector_structs.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, x, y, z, unit="mm"):
    """Init Method

    Args:
        x (ndarray): x coordinates
        y (ndarray): y coordinates
        z (ndarray): z coordinates
        unit (str, optional): Unit of length. Defaults to "mm".

    Raises:
        ValueError: Unit must an SI prefix, e.g. km, m, cm, mm
    """
    super().__init__(x, y, unit=unit)
    self.z = _np.asarray(z)

change_unit(new_unit, get_unit_value=get_unit_value_meter)

Converts point array to a different unit. e.g from 'cm' to 'mm'

Parameters:

Name Type Description Default
new_unit str

unit to be converted to

required
get_unit_value function

Function for checking unit type. Defaults to get_unit_value_meter.

get_unit_value_meter
Source code in src/pymagnet/utils/_vector_structs.py
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
def change_unit(self, new_unit, get_unit_value=get_unit_value_meter):
    """Converts point array to a different unit. e.g from 'cm' to 'mm'

    Args:
        new_unit (str): unit to be converted to
        get_unit_value (function, optional): Function for checking unit type. Defaults to get_unit_value_meter.
    """
    from ..magnets import Cube, Cylinder, Magnet, Mesh, Prism, Sphere

    current_unit_val = get_unit_value(self.get_unit())
    new_unit_val = get_unit_value(new_unit)
    if current_unit_val is None or new_unit_val is None:
        raise ValueError(
            f"Cannot convert from '{self.get_unit()}' to '{new_unit}': "
            "unrecognised unit."
        )
    scale_val = current_unit_val / new_unit_val
    self.x *= scale_val
    self.y *= scale_val
    self.z *= scale_val
    self.unit = new_unit

    for magnet in Magnet.instances:
        magnet.center = scale_val * magnet.center
        if issubclass(magnet.__class__, Prism):
            magnet.a = magnet.a * scale_val
            magnet.b = magnet.b * scale_val
            magnet.c = magnet.c * scale_val
            magnet.width = magnet.width * scale_val
            magnet.height = magnet.height * scale_val
            magnet.depth = magnet.depth * scale_val
        elif issubclass(magnet.__class__, Cube):
            magnet.a = magnet.a * scale_val
            magnet.width = magnet.width * scale_val
        elif issubclass(magnet.__class__, Cylinder):
            magnet.radius = magnet.radius * scale_val
            magnet.length = magnet.length * scale_val
        elif issubclass(magnet.__class__, Sphere):
            magnet.radius = magnet.radius * scale_val
        elif issubclass(magnet.__class__, Mesh):
            magnet.mesh_vectors = magnet.mesh_vectors * scale_val

rotate(alpha, beta, gamma)

Rotates set of Point_Array3 points by angles alpha, beta, gamma

Parameters:

Name Type Description Default
alpha float

Angle to rotate about z in degrees

required
beta float

Angle to rotate about y in degrees

required
gamma float

Angle to rotate about x in degrees

required

Returns:

Type Description
Point_Array3

rotated set of points

Source code in src/pymagnet/utils/_vector_structs.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def rotate(self, alpha, beta, gamma):
    """Rotates set of Point_Array3 points by angles alpha, beta, gamma

    Args:
        alpha (float): Angle to rotate about z in degrees
        beta (float): Angle to rotate about y in degrees
        gamma (float): Angle to rotate about x in degrees

    Returns:
        Point_Array3: rotated set of points
    """
    q_fwd = Quaternion.gen_rotation_quaternion(
        _np.deg2rad(alpha), _np.deg2rad(beta), _np.deg2rad(gamma)
    )

    pos_vec = Quaternion._prepare_vector(self.x, self.y, self.z)
    x_rot, y_rot, z_rot = q_fwd * pos_vec
    return Point_Array3(
        x=x_rot.reshape(self.x.shape),
        y=y_rot.reshape(self.x.shape),
        z=z_rot.reshape(self.x.shape),
    )

Global Constants PI, PI/2, PI/4, ยต0, and three internally used constants: FP_CUTOFF = 1e-8, ALIGN_CUTOFF = 1e-5, MAG_TOL = 1e-4