Skip to content

Commit 671d19b

Browse files
authored
Add a new Sphere class as a special case of ellipsoids (#602)
Add a new `Sphere` class as a special case of ellipsoids with `a == b == c`. Implement gravity and magnetic forward modelling for the sphere, both inside and outside of the body. Extend tests to `Sphere` objects. Use the forward modelling of the `Sphere` to compare the fields of ellipsoids against it.
1 parent b6086a1 commit 671d19b

File tree

8 files changed

+581
-219
lines changed

8 files changed

+581
-219
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Ellipsoids:
9393

9494
OblateEllipsoid
9595
ProlateEllipsoid
96+
Sphere
9697
TriaxialEllipsoid
9798

9899
Layers and meshes:

harmonica/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ._forward.ellipsoids import (
2222
OblateEllipsoid,
2323
ProlateEllipsoid,
24+
Sphere,
2425
TriaxialEllipsoid,
2526
)
2627
from ._forward.point import point_gravity

harmonica/_forward/ellipsoid_gravity.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ def _compute_gravity_ellipsoid(
152152
# Mask internal points
153153
internal = is_internal(x, y, z, a, b, c)
154154

155+
if a == b == c:
156+
# Fallback to sphere equations which are simpler
157+
factor = -4 / 3 * np.pi * a**3 * GRAVITATIONAL_CONST * density
158+
gx, gy, gz = tuple(np.zeros_like(x) for _ in range(3))
159+
160+
gx[internal] = factor * x[internal] / a**3
161+
gy[internal] = factor * y[internal] / a**3
162+
gz[internal] = factor * z[internal] / a**3
163+
164+
r = np.sqrt(x[~internal] ** 2 + y[~internal] ** 2 + z[~internal] ** 2)
165+
gx[~internal] = factor * x[~internal] / r**3
166+
gy[~internal] = factor * y[~internal] / r**3
167+
gz[~internal] = factor * z[~internal] / r**3
168+
169+
return gx, gy, gz
170+
155171
# Compute lambda on all observation points:
156172
# Make it zero on internal points, calculate it for external points.
157173
lambda_ = np.zeros_like(x, dtype=np.float64)

harmonica/_forward/ellipsoid_magnetics.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ def get_demagnetization_tensor_internal(a: float, b: float, c: float):
365365
n_diagonal = _demag_tensor_prolate_internal(a, b)
366366
elif a < b and b == c:
367367
n_diagonal = _demag_tensor_oblate_internal(a, b)
368+
elif a == b == c:
369+
n_diagonal = 1 / 3 * np.ones(3)
368370
else:
369371
msg = "Could not determine ellipsoid type for values given."
370372
raise ValueError(msg)
@@ -528,23 +530,38 @@ def get_demagnetization_tensor_external(
528530
# Allocate array for all demagnetization tensors (one for each observation point)
529531
demag_tensors = np.empty((x.size, 3, 3), dtype=np.float64)
530532

531-
# Calculate lambda and other quantities needed to build the tensors
532-
lambda_ = calculate_lambda(x, y, z, a, b, c)
533-
ellip_integrals = get_elliptical_integrals(a, b, c, lambda_)
534-
deriv_ellip_integrals = get_derivatives_of_elliptical_integrals(a, b, c, lambda_)
535-
derivs_lmbda = _spatial_deriv_lambda(x, y, z, a, b, c, lambda_)
536-
537533
coords = (x, y, z)
538-
for i, j in itertools.product(range(3), range(3)):
539-
if i == j:
540-
demag_tensors[:, i, i] = ((a * b * c) / 2) * (
541-
derivs_lmbda[i] * deriv_ellip_integrals[i] * coords[i]
542-
+ ellip_integrals[i]
543-
)
544-
else:
545-
demag_tensors[:, i, j] = ((a * b * c) / 2) * (
546-
derivs_lmbda[i] * deriv_ellip_integrals[j] * coords[j]
547-
)
534+
535+
if a == b == c:
536+
r = np.sqrt(x**2 + y**2 + z**2)
537+
r5, r3 = r**5, r**3
538+
factor = -(a**3 / 3)
539+
540+
for i, j in itertools.product(range(3), range(3)):
541+
if i == j:
542+
demag_tensors[:, i, i] = factor * (3 * coords[i] ** 2 / r5 - 1 / r3)
543+
else:
544+
demag_tensors[:, i, j] = factor * (3 * coords[i] * coords[j] / r5)
545+
546+
else:
547+
# Calculate lambda and other quantities needed to build the tensors
548+
lambda_ = calculate_lambda(x, y, z, a, b, c)
549+
ellip_integrals = get_elliptical_integrals(a, b, c, lambda_)
550+
deriv_ellip_integrals = get_derivatives_of_elliptical_integrals(
551+
a, b, c, lambda_
552+
)
553+
derivs_lmbda = _spatial_deriv_lambda(x, y, z, a, b, c, lambda_)
554+
555+
for i, j in itertools.product(range(3), range(3)):
556+
if i == j:
557+
demag_tensors[:, i, i] = ((a * b * c) / 2) * (
558+
derivs_lmbda[i] * deriv_ellip_integrals[i] * coords[i]
559+
+ ellip_integrals[i]
560+
)
561+
else:
562+
demag_tensors[:, i, j] = ((a * b * c) / 2) * (
563+
derivs_lmbda[i] * deriv_ellip_integrals[j] * coords[j]
564+
)
548565

549566
return demag_tensors
550567

harmonica/_forward/ellipsoids.py

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ class BaseEllipsoid:
2626
This class is not meant to be instantiated.
2727
"""
2828

29-
yaw: float
30-
pitch: float
31-
roll: float
32-
3329
@property
3430
def density(self) -> float | None:
3531
"""Density of the ellipsoid in :math:`kg/m^3`."""
@@ -482,3 +478,111 @@ def _check_semiaxes_lenghts(self, a, b):
482478
f"expected a < b (= c ) but got a = {a}, b = {b}"
483479
)
484480
raise ValueError(msg)
481+
482+
483+
class Sphere(BaseEllipsoid):
484+
"""
485+
Homogeneous sphere.
486+
487+
Represent a sphere as a particular type of ellipsoid where ``a == b == c``.
488+
489+
Parameters
490+
----------
491+
a : float
492+
Radius or semiaxes lengths in meters.
493+
center : tuple of float
494+
Coordinates of the center of the sphere in the following order: `easting`,
495+
`northing`, `upward`. All must be in meters.
496+
density : float or None, optional
497+
Density of the sphere in :math:`kg/m^3`.
498+
susceptibility : float, (3, 3) array or None, optional
499+
Magnetic susceptibility of the sphere in SI units.
500+
A single float represents isotropic susceptibility in the body.
501+
A (3, 3) array represents the susceptibility tensor to account for anisotropy.
502+
If None, zero susceptibility will be assigned to the sphere.
503+
remanent_mag : (3,) array or None, optional
504+
Remanent magnetization vector of the sphere in A/m units. Its components
505+
are defined in the easting-northing-upward coordinate system and should be
506+
passed in that order.
507+
If None, no remanent magnetization will be assigned to the sphere.
508+
509+
Notes
510+
-----
511+
All semiaxes (``a``, ``b`` and ``c``) are equal to each other.
512+
All rotation angles (``yaw``, ``pitch`` and ``roll``) are equal to zero for the
513+
sphere, since sphere is invariant to rotations.
514+
"""
515+
516+
def __init__(
517+
self,
518+
a: float,
519+
center: tuple[float, float, float],
520+
*,
521+
density: float | None = None,
522+
susceptibility: float | npt.NDArray | None = None,
523+
remanent_mag: npt.NDArray | None = None,
524+
):
525+
self._check_positive_semiaxis(a, "a")
526+
self._a = a
527+
self.center = center
528+
529+
# Physical properties of the sphere
530+
self.density = density
531+
self.susceptibility = susceptibility
532+
self.remanent_mag = remanent_mag
533+
534+
@property
535+
def a(self) -> float:
536+
"""Length of the first semiaxis."""
537+
return self._a
538+
539+
@a.setter
540+
def a(self, value: float) -> None:
541+
self._check_positive_semiaxis(value, "a")
542+
self._a = value
543+
544+
@property
545+
def b(self) -> float:
546+
"""Length of the second semiaxis."""
547+
return self._a
548+
549+
@property
550+
def c(self) -> float:
551+
"""Length of the third semiaxis."""
552+
return self._a
553+
554+
@property
555+
def radius(self) -> float:
556+
"""Sphere radius."""
557+
return self._a
558+
559+
@property
560+
def yaw(self):
561+
"""Yaw angle, equal to zero."""
562+
return 0.0
563+
564+
@property
565+
def pitch(self):
566+
"""Pitch angle, equal to zero."""
567+
return 0.0
568+
569+
@property
570+
def roll(self):
571+
"""Roll angle, equal to zero."""
572+
return 0.0
573+
574+
@property
575+
def rotation_matrix(self) -> npt.NDArray:
576+
"""
577+
Create a rotation matrix for the sphere.
578+
579+
.. important:
580+
581+
The rotation matrix for the sphere is always the identity matrix.
582+
583+
Returns
584+
-------
585+
rotation_matrix : (3, 3) array
586+
Identity matrix.
587+
"""
588+
return np.eye(3, dtype=np.float64)

0 commit comments

Comments
 (0)