Skip to content

Commit 69303bd

Browse files
authored
Merge pull request #15 from magpylib/dipole_force
Dipole force
2 parents 692cb8c + b1ac26f commit 69303bd

File tree

6 files changed

+174
-17
lines changed

6 files changed

+174
-17
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
# Unpublished
1+
# v0.4.0 (2025/02/03)
2+
3+
- Implementation of Dipole as target
4+
- Bugfix for magnets with meshing parameter = 1
5+
- Additional tests for Dipole extension
26

37
# v0.3.1 (2024/10/30)
48

src/magpylib_force/force.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from magpylib._src.obj_classes.class_magnet_Cylinder import Cylinder
1515
from magpylib._src.obj_classes.class_magnet_CylinderSegment import CylinderSegment
1616
from magpylib._src.obj_classes.class_magnet_Sphere import Sphere
17+
from magpylib._src.obj_classes.class_misc_Dipole import Dipole
1718

1819
from magpylib_force.meshing import mesh_target
1920
from magpylib_force.utility import check_input_anchor, check_input_targets
@@ -35,8 +36,8 @@ def getFT(sources, targets, anchor=None, eps=1e-5, squeeze=True):
3536
or a 1D list of l sources and/or collection objects.
3637
3738
targets: single object or 1D list of t objects that are Sphere, Cuboid, Polyline,
38-
Cylinder, or CylinderSegment. Force and Torque acting on targets in the magnetic
39-
field generated by the sources will be computed. A target must have a valid
39+
Cylinder, CylinderSegment, or Dipole. Force and Torque acting on targets in the magnetic
40+
field generated by the sources will be computed. A target (except of Dipole) must have a valid
4041
`meshing` parameter.
4142
4243
anchor: array_like, default=None
@@ -65,14 +66,15 @@ def getFT(sources, targets, anchor=None, eps=1e-5, squeeze=True):
6566
n = len(targets)
6667

6768
# split targets into lists of similar types
68-
TARGET_TYPES = [Cuboid, Polyline, Sphere, Cylinder, CylinderSegment, Circle]
69+
TARGET_TYPES = [Cuboid, Polyline, Sphere, Cylinder, CylinderSegment, Circle, Dipole]
6970
getFT_FUNCS = [
7071
getFTmagnet,
7172
getFTcurrent,
7273
getFTmagnet,
7374
getFTmagnet,
7475
getFTmagnet,
7576
getFTcurrent_circ,
77+
getFTdipole,
7678
]
7779
objects = [[] for _ in TARGET_TYPES]
7880
orders = [[] for _ in TARGET_TYPES]
@@ -202,6 +204,8 @@ def getFTmagnet(sources, targets, eps=1e-5, anchor=None):
202204
POSS[insti[i] : insti[i + 1], j] = mesh + ev + tgt.position
203205

204206
BB = magpy.getB(sources, POSS, sumup=True)
207+
if BB.ndim == 2:
208+
BB = np.expand_dims(BB, axis=0)
205209
gradB = (BB[:, 1::2] - BB[:, 2::2]) / (2 * eps)
206210
gradB = np.swapaxes(gradB, 0, 1)
207211

@@ -333,3 +337,71 @@ def getFTcurrent(sources, targets, anchor=None, eps=None): # noqa: ARG001
333337
)
334338

335339
return np.array((F, -T))
340+
341+
342+
def getFTdipole(sources, targets, anchor=None, eps=None):
343+
"""
344+
Compute force and torque acting on magnetic dipoles
345+
346+
Parameters
347+
----------
348+
sources: source and collection objects or 1D list thereof
349+
Sources that generate the magnetic field. Can be a single source (or collection)
350+
or a 1D list of l sources and/or collection objects.
351+
352+
targets: Dipole object or 1D list of Dipole objects
353+
Force and Torque acting on targets in the magnetic field generated by the sources
354+
will be computed.
355+
356+
eps: float, default=1e-5
357+
The magnetic field gradient is computed using finite differences (FD). eps is
358+
the FD step size. A good value is 1e-5 * characteristic system size (magnet size,
359+
distance between sources and targets, ...).
360+
361+
anchor: array_like, default=None
362+
The Force adds to the Torque via the anchor point. For a freely floating magnet
363+
this would be the barycenter. If `anchor=None`, this part of the Torque computation
364+
is omitted.
365+
"""
366+
# number of magnets
367+
tgt_number = len(targets)
368+
369+
# field computation positions (1xfor B, 6x for gradB)
370+
POSS = np.zeros((tgt_number, 7, 3))
371+
372+
# moment of each instance
373+
MOM = np.zeros((tgt_number, 3))
374+
375+
# MISSING: eps should be defined relative to the sizes of the objects
376+
eps_vec = np.array(
377+
[
378+
(0, 0, 0),
379+
(eps, 0, 0),
380+
(-eps, 0, 0),
381+
(0, eps, 0),
382+
(0, -eps, 0),
383+
(0, 0, eps),
384+
(0, 0, -eps),
385+
]
386+
)
387+
388+
for i, tgt in enumerate(targets):
389+
dipole_mom = tgt.orientation.apply(tgt.moment)
390+
MOM[i] = dipole_mom
391+
392+
for j, ev in enumerate(eps_vec):
393+
POSS[i, j] = ev + tgt.position
394+
395+
BB = magpy.getB(sources, POSS, sumup=True)
396+
if BB.ndim == 2:
397+
BB = np.expand_dims(BB, axis=0)
398+
gradB = (BB[:, 1::2] - BB[:, 2::2]) / (2 * eps)
399+
gradB = np.swapaxes(gradB, 0, 1)
400+
401+
F = np.sum((gradB * MOM), axis=2).T
402+
# Ts = np.zeros((no_inst,3))
403+
T = np.cross(BB[:, 0], MOM)
404+
if anchor is not None:
405+
T -= np.cross(POSS[:, 0] - anchor, F)
406+
407+
return np.array((F, -T))

src/magpylib_force/utility.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from magpylib._src.obj_classes.class_magnet_Cylinder import Cylinder
1414
from magpylib._src.obj_classes.class_magnet_CylinderSegment import CylinderSegment
1515
from magpylib._src.obj_classes.class_magnet_Sphere import Sphere
16+
from magpylib._src.obj_classes.class_misc_Dipole import Dipole
1617

1718

1819
def check_input_anchor(anchor):
@@ -43,7 +44,7 @@ def check_input_targets(targets):
4344
targets = [targets]
4445
for t in targets:
4546
if not isinstance(
46-
t, Cuboid | Polyline | Sphere | Cylinder | CylinderSegment | Circle
47+
t, Cuboid | Polyline | Sphere | Cylinder | CylinderSegment | Circle | Dipole
4748
):
4849
msg = (
4950
"Bad `targets` input for getFT."
@@ -52,16 +53,21 @@ def check_input_targets(targets):
5253
f" Instead received type {type(t)} target."
5354
)
5455
raise ValueError(msg)
55-
if not hasattr(t, "meshing"):
56-
msg = (
57-
"Missing input for getFT `targets`."
58-
" `targets` must have the `meshing` parameter set."
59-
)
60-
raise ValueError(msg)
61-
if not isinstance(t, Polyline) and np.isscalar(t.meshing) and t.meshing < 20:
62-
warnings.warn(
63-
"Input parameter `meshing` is set to a low value which will result in "
64-
"inaccurate computation of force and torque.",
65-
stacklevel=2,
66-
)
56+
if not isinstance(t, Dipole):
57+
if not hasattr(t, "meshing"):
58+
msg = (
59+
"Missing input for getFT `targets`."
60+
" `targets` must have the `meshing` parameter set."
61+
)
62+
raise ValueError(msg)
63+
if (
64+
not isinstance(t, Polyline)
65+
and np.isscalar(t.meshing)
66+
and t.meshing < 20
67+
):
68+
warnings.warn(
69+
"Input parameter `meshing` is set to a low value which will result in "
70+
"inaccurate computation of force and torque.",
71+
stacklevel=2,
72+
)
6773
return targets

tests/test_basics.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import magpylib as magpy
44
import numpy as np
5+
from scipy.spatial.transform import Rotation as R
56

67
from magpylib_force import getFT
78

@@ -57,3 +58,20 @@ def test_rotation2():
5758
FT = getFT(s1, [c1, c2], anchor=(0, 0, 0))
5859

5960
np.testing.assert_allclose(FT[0], FT[1])
61+
62+
63+
def test_orientation():
64+
"""
65+
test if dipole with orientation gives same result as rotated magnetic moment
66+
"""
67+
mm, md = np.array((0.976, 4.304, 2.055)), np.array((0.878, -1.527, 2.918))
68+
pm, pd = np.array((-1.248, 7.835, 9.273)), np.array((-2.331, 5.835, 0.578))
69+
70+
magnet = magpy.magnet.Cuboid(position=pm, dimension=(1, 2, 3), polarization=mm)
71+
r = R.from_euler("xyz", (25, 65, 150), degrees=True)
72+
dipole1 = magpy.misc.Dipole(position=pd, moment=md, orientation=r)
73+
dipole2 = magpy.misc.Dipole(position=pd, moment=r.apply(md))
74+
75+
F = getFT(magnet, [dipole1, dipole2], anchor=(0, 0, 0))
76+
77+
np.testing.assert_allclose(F[0], F[1])

tests/test_physics.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,36 @@ def test_physics_force_between_cocentric_loops():
242242
F_ana = pf * ((2 - k2) / (1 - k2) * ellipe(k**2) - 2 * ellipk(k**2))
243243

244244
assert abs((F_num - F_ana) / (F_num + F_ana)) < 1e-5
245+
246+
247+
def test_physics_force_between_two_dipoles():
248+
"""
249+
Compare force between two magnetic Dipoles with well-known formula
250+
(e.g. https://en.wikipedia.org/wiki/Magnetic_dipole%E2%80%93dipole_interaction)
251+
"""
252+
m1, m2 = np.array((0.976, 4.304, 2.055)), np.array((0.878, -1.527, 2.918))
253+
p1, p2 = np.array((-1.248, 7.835, 9.273)), np.array((-2.331, 5.835, 0.578))
254+
255+
# numerical solution
256+
dipole1 = magpy.misc.Dipole(position=p1, moment=m1)
257+
dipole2 = magpy.misc.Dipole(position=p2, moment=m2)
258+
F_num = getFT(dipole1, dipole2, anchor=(0, 0, 0))[0, :]
259+
# F_num = getFT([dipole1], [dipole2, dipole2])
260+
261+
# analytical solution
262+
r = p2 - p1
263+
r_abs = np.linalg.norm(r)
264+
r_unit = r / r_abs
265+
F_ana = (
266+
3
267+
* magpy.mu_0
268+
/ (4 * np.pi * r_abs**4)
269+
* (
270+
np.cross(np.cross(r_unit, m1), m2)
271+
+ np.cross(np.cross(r_unit, m2), m1)
272+
- 2 * r_unit * np.dot(m1, m2)
273+
+ 5 * r_unit * (np.dot(np.cross(r_unit, m1), np.cross(r_unit, m2)))
274+
)
275+
)
276+
277+
np.testing.assert_allclose(F_num, F_ana, rtol=1e-8, atol=1e-8)

tests/test_self_consistency.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,27 @@ def test_consistency_polyline_circle():
264264
F2, T2 = getFT(src, loop2, anchor=(0, 0, 0))
265265
assert abs(np.linalg.norm(F1 - F2) / np.linalg.norm(F1 + F2)) < 1e-7
266266
assert abs(np.linalg.norm(T1 - T2) / np.linalg.norm(T1 + T2)) < 1e-7
267+
268+
269+
def test_consistency_sphere_dipole():
270+
"""
271+
force on sphere and dipole should be the same in nearly homogeneous field
272+
"""
273+
274+
src = magpy.current.Circle(diameter=10, current=123)
275+
pos = (0, 0, 0)
276+
diameter = 0.5
277+
magnetization_sphere = np.array((1e6, 2e6, 3e6))
278+
moment_dipole = magnetization_sphere * diameter**3 / 6 * np.pi
279+
280+
sphere = magpy.magnet.Sphere(
281+
diameter=diameter, magnetization=magnetization_sphere, position=pos
282+
)
283+
sphere.meshing = 100
284+
285+
dipole = magpy.misc.Dipole(position=pos, moment=moment_dipole)
286+
287+
FT_sphere = getFT(src, sphere, anchor=(0, 0, 0))
288+
FT_dipole = getFT(src, dipole, anchor=(0, 0, 0))
289+
290+
np.testing.assert_allclose(FT_sphere, FT_dipole, rtol=1e-6, atol=1e-6)

0 commit comments

Comments
 (0)