Skip to content

Commit 0748e2d

Browse files
committed
include Sphere, rework force workflow
1 parent 9555375 commit 0748e2d

File tree

4 files changed

+209
-53
lines changed

4 files changed

+209
-53
lines changed

magpylib_force/force.py

Lines changed: 148 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import numpy as np
22
import magpylib as magpy
3+
from magpylib_force.meshing import mesh_target
34
from magpylib_force.meshing import mesh_cuboid
45
from magpylib_force.utility import check_input_anchor
56
from magpylib_force.utility import check_input_targets
67
from magpylib._src.obj_classes.class_current_Polyline import Polyline
78
from magpylib._src.obj_classes.class_magnet_Cuboid import Cuboid
9+
from magpylib._src.obj_classes.class_magnet_Sphere import Sphere
810

911

1012
def getFT(sources, targets, anchor=None, eps=1e-5, squeeze=True):
@@ -21,62 +23,90 @@ def getFT(sources, targets, anchor=None, eps=1e-5, squeeze=True):
2123
targets: single object or 1D list of t objects that are in [Cuboid, Polyline]
2224
Force and Torque acting on targets in the magnetic field generated by the sources
2325
will be computed. A target must have a valid `meshing` parameter.
24-
25-
eps: float, default=1e-5
26-
For magnet-targets the magnetic field gradient is computed using finite
27-
differences (FD). `eps` is the FD step size. A good value
28-
is 1e-5 * characteristic system size (magnet size, distance between sources
29-
and targets, ...).
3026
3127
anchor: array_like, default=None
3228
The Force adds to the Torque via the anchor point. For a freely floating magnet
3329
this would be the barycenter. If `anchor=None`, this part of the Torque computation
3430
is ommitted.
3531
32+
eps: float, default=1e-5
33+
This is only used for magnet targets for computing the magnetic field gradient
34+
using finite differences (FD). `eps` is the FD step size. A good value
35+
is 1e-5 * characteristic_system_size (magnet size, distance between sources
36+
and targets, ...).
37+
3638
squeeze: bool, default=True
3739
The output of the computation has the shape (n,3) where n corresponds to the number
3840
of targets. By default this is reduced to (3,) when there is only one target.
3941
4042
Returns
4143
-------
42-
Force-Torque as ndarray of shape (2,3), of (t,2,3) when t targets are given
44+
Force-Torque as ndarray of shape (2,3), or (t,2,3) when t targets are given
4345
"""
4446
anchor = check_input_anchor(anchor)
4547
targets = check_input_targets(targets)
46-
# MISSING: allow Collections
48+
# MISSING: allow Collections as targets
4749

4850
n = len(targets)
4951

50-
cuboids, order_cub = [], []
51-
polylines, order_poly = [], []
52-
for i,t in enumerate(targets):
53-
if isinstance(t, Cuboid):
54-
cuboids.append(t)
55-
order_cub.append(i)
56-
elif isinstance(t, Polyline):
57-
polylines.append(t)
58-
order_poly.append(i)
52+
# split targets into lists of similar types
53+
TARGET_TYPES = [Cuboid, Polyline, Sphere]
54+
getFT_FUNCS = [getFTmagnet, getFTcurrent, getFTmagnet]
55+
objects = [[] for _ in TARGET_TYPES]
56+
orders = [[] for _ in TARGET_TYPES]
5957

58+
for i,tgt in enumerate(targets):
59+
for j,ttyp in enumerate(TARGET_TYPES):
60+
if isinstance(tgt, ttyp):
61+
objects[j].append(tgt)
62+
orders[j].append(i)
63+
64+
# allocate FT
6065
FT = np.zeros((n, 2, 3))
61-
if cuboids:
62-
FT_cube = getFTcube(sources, cuboids, eps=eps, anchor=anchor, squeeze=False)
63-
FT_cube = np.swapaxes(FT_cube, 0, 1)
64-
for ft,o in zip(FT_cube, order_cub):
65-
FT[o] = ft
66-
if polylines:
67-
FT_poly = getFTwire(sources, polylines, anchor=anchor, squeeze=False)
68-
FT_poly = np.swapaxes(FT_poly, 0, 1)
69-
for ft,o in zip(FT_poly, order_poly):
70-
FT[o] = ft
71-
#FT = np.swapaxes(FT, 0, 1)
66+
67+
# FT-computation and broadcasting
68+
for i in range(len(TARGET_TYPES)):
69+
if objects[i]:
70+
ft_part = getFT_FUNCS[i](sources, objects[i], eps=eps, anchor=anchor)
71+
ft_part = np.swapaxes(ft_part, 0, 1)
72+
for ft,j in zip(ft_part, orders[i]):
73+
FT[j] = ft
74+
7275
if squeeze:
7376
return np.squeeze(FT)
7477
return FT
7578

7679

77-
def getFTcube(sources, targets, eps=1e-5, anchor=None, squeeze=True):
80+
def mesh_instances(targets):
7881
"""
79-
Compute force and torque acting on a Cuboid magnet.
82+
utility for getFTmagnet: compute number of mesh instances for each target magnet
83+
targets: list of target magnet objects
84+
returns: int
85+
"""
86+
if isinstance(targets[0], Cuboid):
87+
return np.array([np.prod(tgt.meshing) for tgt in targets])
88+
elif isinstance(targets[0], Sphere):
89+
SPH_MESH = (0,1,8,19,32,81,136,179,280,389,552,739,912,1189,1472,1791,2176,2553,3112,3695,4224)
90+
return np.array([SPH_MESH[tgt.meshing] for tgt in targets])
91+
raise RuntimeError("fktn `mesh_instances` - I shouldt be here.")
92+
93+
94+
def volume(target):
95+
"""
96+
utility for getFTmagnet: compute physical volume of target magnets
97+
target: target magnet object
98+
return: volume, float
99+
"""
100+
if isinstance(target, Cuboid):
101+
return np.prod(target.dimension)
102+
elif isinstance(target, Sphere):
103+
return target.diameter**3 * np.pi / 6
104+
raise RuntimeError("fktn `volume` - I shouldt be here.")
105+
106+
107+
def getFTmagnet(sources, targets, eps=1e-5, anchor=None):
108+
"""
109+
Compute force and torque acting on magnets of same type
80110
81111
Parameters
82112
----------
@@ -97,17 +127,12 @@ def getFTcube(sources, targets, eps=1e-5, anchor=None, squeeze=True):
97127
The Force adds to the Torque via the anchor point. For a freely floating magnet
98128
this would be the barycenter. If `anchor=None`, this part of the Torque computation
99129
is ommitted.
100-
101-
squeeze: bool, default=True
102-
The output of the computation has the shape (n,3) where n corresponds to the number
103-
of targets. By default this is reduced to (3,) when there is only one target.
104130
"""
105-
106-
# number of Cuboids
131+
# number of magnets
107132
tgt_number = len(targets)
108133

109-
# number of instances of each Cuboid
110-
inst_numbers = np.array([np.prod(tgt.meshing) for tgt in targets])
134+
# number of instances of each magnet
135+
inst_numbers = mesh_instances(targets)
111136

112137
# total number of instances
113138
no_inst = np.sum(inst_numbers)
@@ -121,15 +146,15 @@ def getFTcube(sources, targets, eps=1e-5, anchor=None, squeeze=True):
121146
# moment of each instance
122147
MOM = np.zeros((no_inst,3))
123148

124-
# MISSING: eps should be defined relative to the sizes of the system
149+
# MISSING: eps should be defined relative to the sizes of the objects
125150
eps_vec = np.array([(0,0,0), (eps,0,0), (-eps,0,0), (0,eps,0), (0,-eps,0), (0,0,eps), (0,0,-eps)])
126151

127152
for i,tgt in enumerate(targets):
128-
tgt_vol = np.prod(tgt.dimension)
153+
tgt_vol = volume(tgt)
129154
inst_mom = tgt.magnetization * tgt_vol / inst_numbers[i]
130155
MOM[insti[i]:insti[i+1]] = inst_mom
131156

132-
mesh = mesh_cuboid(tgt)
157+
mesh = mesh_target(tgt)
133158
for j,ev in enumerate(eps_vec):
134159
POSS[insti[i]:insti[i+1],j] = mesh + ev + tgt.position
135160

@@ -146,17 +171,94 @@ def getFTcube(sources, targets, eps=1e-5, anchor=None, squeeze=True):
146171
T = np.array([np.sum(Ts[insti[i]:insti[i+1]],axis=0) for i in range(tgt_number)])
147172
F = np.array([np.sum(Fs[insti[i]:insti[i+1]],axis=0) for i in range(tgt_number)])
148173

149-
if squeeze:
150-
F = np.squeeze(F)
151-
T = np.squeeze(T)
152-
153174
return np.array((F, T))
154175

155176

156177

157-
def getFTwire(sources, targets, anchor=None, squeeze=True):
178+
# def getFTcube(sources, targets, eps=1e-5, anchor=None, squeeze=True):
179+
# """
180+
# Compute force and torque acting on a Cuboid magnet.
181+
182+
# Parameters
183+
# ----------
184+
# sources: source and collection objects or 1D list thereof
185+
# Sources that generate the magnetic field. Can be a single source (or collection)
186+
# or a 1D list of l sources and/or collection objects.
187+
188+
# targets: Cuboid object or 1D list of Cuboid objects
189+
# Force and Torque acting on targets in the magnetic field generated by the sources
190+
# will be computed. A target must have a valid `meshing` parameter.
191+
192+
# eps: float, default=1e-5
193+
# The magnetic field gradient is computed using finite differences (FD). eps is
194+
# the FD step size. A good value is 1e-5 * characteristic system size (magnet size,
195+
# distance between sources and targets, ...).
196+
197+
# anchor: array_like, default=None
198+
# The Force adds to the Torque via the anchor point. For a freely floating magnet
199+
# this would be the barycenter. If `anchor=None`, this part of the Torque computation
200+
# is ommitted.
201+
202+
# squeeze: bool, default=True
203+
# The output of the computation has the shape (n,3) where n corresponds to the number
204+
# of targets. By default this is reduced to (3,) when there is only one target.
205+
# """
206+
207+
# # number of Cuboids
208+
# tgt_number = len(targets)
209+
210+
# # number of instances of each Cuboid
211+
# inst_numbers = np.array([np.prod(tgt.meshing) for tgt in targets])
212+
213+
# # total number of instances
214+
# no_inst = np.sum(inst_numbers)
215+
216+
# # cumsum of number of instances (used for indexing)
217+
# insti = np.r_[0, np.cumsum(inst_numbers)]
218+
219+
# # field computation positions (1xfor B, 6x for gradB)
220+
# POSS = np.zeros((no_inst,7,3))
221+
222+
# # moment of each instance
223+
# MOM = np.zeros((no_inst,3))
224+
225+
# # MISSING: eps should be defined relative to the sizes of the system
226+
# eps_vec = np.array([(0,0,0), (eps,0,0), (-eps,0,0), (0,eps,0), (0,-eps,0), (0,0,eps), (0,0,-eps)])
227+
228+
# for i,tgt in enumerate(targets):
229+
# tgt_vol = np.prod(tgt.dimension)
230+
# inst_mom = tgt.magnetization * tgt_vol / inst_numbers[i]
231+
# MOM[insti[i]:insti[i+1]] = inst_mom
232+
233+
# mesh = mesh_cuboid(tgt)
234+
# for j,ev in enumerate(eps_vec):
235+
# POSS[insti[i]:insti[i+1],j] = mesh + ev + tgt.position
236+
237+
# BB = magpy.getB(sources, POSS, sumup=True)
238+
# gradB = (BB[:,1::2]-BB[:,2::2]) / (2*eps)
239+
# gradB = np.swapaxes(gradB,0,1)
240+
241+
# Fs = np.sum((gradB*MOM),axis=2).T
242+
# #Ts = np.zeros((no_inst,3))
243+
# Ts = np.cross(BB[:,0], MOM)
244+
# if anchor is not None:
245+
# Ts -= np.cross(POSS[:,0]-anchor, Fs)
246+
247+
# T = np.array([np.sum(Ts[insti[i]:insti[i+1]],axis=0) for i in range(tgt_number)])
248+
# F = np.array([np.sum(Fs[insti[i]:insti[i+1]],axis=0) for i in range(tgt_number)])
249+
250+
# if squeeze:
251+
# F = np.squeeze(F)
252+
# T = np.squeeze(T)
253+
254+
# return np.array((F, T))
255+
256+
257+
258+
def getFTcurrent(sources, targets, anchor=None, eps=None):
158259
"""
159260
compute force acting on tgt Polyline
261+
eps is a dummy variable that is not used
160262
161263
info:
162264
targets = Polyline objects
@@ -218,9 +320,4 @@ def getFTwire(sources, targets, anchor=None, squeeze=True):
218320
# sumup force for every target
219321
F = np.array([np.sum(F[insti[i]:insti[i+1]],axis=0) for i in range(tgt_number)])
220322

221-
# squeeze output
222-
if squeeze:
223-
F = np.squeeze(F)
224-
T = np.squeeze(T)
225-
226323
return np.array((F, T))

magpylib_force/meshing.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
22
import itertools
33
import math as m
44
import pint
5+
from magpylib._src.obj_classes.class_magnet_Cuboid import Cuboid
6+
from magpylib._src.obj_classes.class_magnet_Sphere import Sphere
7+
8+
def mesh_target(object):
9+
"""
10+
create mesh for target objects
11+
"""
12+
if isinstance(object, Cuboid):
13+
return mesh_cuboid(object)
14+
elif isinstance(object, Sphere):
15+
return mesh_sphere(object)
16+
raise RuntimeError("fktn `mesh_target`: should not be here!")
17+
18+
19+
def mesh_sphere(object):
20+
"""
21+
create sphere mesh from object meshing parameter
22+
"""
23+
n = object.meshing
24+
dia = object.diameter
25+
a = -dia/2+dia/(2*n)
26+
b = dia/2-dia/(2*n)
27+
c = n*1j
28+
mesh = np.mgrid[a:b:c, a:b:c, a:b:c].T.reshape(n**3,3)
29+
return mesh[np.linalg.norm(mesh, axis=1)<dia/2]
30+
531

632
def mesh_cuboid(object):
733
"""

magpylib_force/utility.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import numpy as np
22
from magpylib._src.obj_classes.class_magnet_Cuboid import Cuboid
33
from magpylib._src.obj_classes.class_current_Polyline import Polyline
4-
4+
from magpylib._src.obj_classes.class_magnet_Sphere import Sphere
55

66
def check_input_anchor(anchor):
77
"""
@@ -23,7 +23,7 @@ def check_input_targets(targets):
2323
if not isinstance(targets, list):
2424
targets = [targets]
2525
for t in targets:
26-
if not isinstance(t, (Cuboid, Polyline)):
26+
if not isinstance(t, (Cuboid, Polyline, Sphere)):
2727
raise ValueError(
2828
"Bad `targets` input for getFT."
2929
" `targets` can only be Cuboids and Polylines."

tests/test_physics.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,36 @@ def test_cube_loop_replacement():
229229

230230
assert np.amax(abs((F1-F2)/(F1+F2)*2))<1e-2
231231
assert np.amax(abs((T1-T2)/(T1+T2)*2))<1e-2
232+
233+
234+
def test_sphere_cube_at_distance():
235+
"""
236+
A sphere and a cuboid with similar volume should see a similar torque and force
237+
at a distance
238+
"""
239+
source = magpy.magnet.Sphere(diameter=1, polarization=(1,2,3))
240+
241+
J = (3,2,1)
242+
pos = (5,-7,11)
243+
244+
cube = magpy.magnet.Cuboid(
245+
dimension=(1,1,1),
246+
polarization=J,
247+
position=pos
248+
)
249+
cube.meshing = (2,2,2)
250+
251+
sphere = magpy.magnet.Sphere(
252+
diameter=(6/np.pi)**(1/3),
253+
polarization=J,
254+
position=pos,
255+
)
256+
sphere.meshing=2
257+
258+
FT = getFT(source, [cube, sphere], anchor=(0,0,0))
259+
260+
errF = (FT[0,0]-FT[1,0])/np.linalg.norm(FT[0,0])
261+
errT = (FT[0,1]-FT[1,1])/np.linalg.norm(FT[0,1])
262+
263+
assert max(abs(errF)) < 1e-5
264+
assert max(abs(errT)) < 1e-5

0 commit comments

Comments
 (0)