Skip to content

Commit d92afbf

Browse files
authored
Add a new create_ellipsoid function (#606)
Add a new `create_ellipsoid` function that takes values of the three semiaxes lenghts (`a`, `b`, and `c`) in any particular order and returns an ellipsoid object of the corresponding type. It takes the `center` as required argument as well, but defaults yaw, pitch, and roll angles to zero. It can also take physical properties of the ellipsoids that are passed through ``kwargs``. Add tests for the new function, and reorganize the ellipsoid code and tests under new `ellipsoids` submodule. **Relevant issues/PRs:** Part of #594
1 parent 36adc68 commit d92afbf

File tree

12 files changed

+287
-28
lines changed

12 files changed

+287
-28
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Ellipsoids:
9191
.. autosummary::
9292
:toctree: generated/
9393

94+
create_ellipsoid
9495
OblateEllipsoid
9596
ProlateEllipsoid
9697
Sphere

harmonica/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
from ._equivalent_sources.spherical import EquivalentSourcesSph
1717
from ._euler_deconvolution import EulerDeconvolution
1818
from ._forward.dipole import dipole_magnetic
19-
from ._forward.ellipsoid_gravity import ellipsoid_gravity
20-
from ._forward.ellipsoid_magnetics import ellipsoid_magnetic
2119
from ._forward.ellipsoids import (
2220
OblateEllipsoid,
2321
ProlateEllipsoid,
2422
Sphere,
2523
TriaxialEllipsoid,
24+
create_ellipsoid,
25+
ellipsoid_gravity,
26+
ellipsoid_magnetic,
2627
)
2728
from ._forward.point import point_gravity
2829
from ._forward.prism_gravity import prism_gravity
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) 2018 The Harmonica Developers.
2+
# Distributed under the terms of the BSD 3-Clause License.
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
#
5+
# This code is part of the Fatiando a Terra project (https://www.fatiando.org)
6+
#
7+
"""
8+
Forward modelling of ellipsoids.
9+
"""
10+
11+
from .ellipsoids import (
12+
OblateEllipsoid,
13+
ProlateEllipsoid,
14+
Sphere,
15+
TriaxialEllipsoid,
16+
create_ellipsoid,
17+
)
18+
from .gravity import ellipsoid_gravity
19+
from .magnetic import ellipsoid_magnetic

harmonica/_forward/ellipsoids.py renamed to harmonica/_forward/ellipsoids/ellipsoids.py

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,134 @@
1414
import numpy as np
1515
import numpy.typing as npt
1616

17-
from .utils import get_rotation_matrix
17+
from ..utils import get_rotation_matrix
18+
19+
20+
def create_ellipsoid(
21+
a: float,
22+
b: float,
23+
c: float,
24+
*,
25+
center: tuple[float, float, float],
26+
yaw: float = 0.0,
27+
pitch: float = 0.0,
28+
roll: float = 0.0,
29+
**kwargs,
30+
) -> "BaseEllipsoid":
31+
"""
32+
Create an ellipsoid given its three semiaxes lenghts.
33+
34+
This function returns an ellipsoid object of the correct type based on the values of
35+
the three semiaxes lenghts. Semiaxes lengths can be passed in any order.
36+
37+
Parameters
38+
----------
39+
a, b, c: float
40+
Semiaxes lenghts in meters. They can be passed in any particular order. They
41+
must all be positive values.
42+
center : tuple of float
43+
Coordinates of the center of the ellipsoid in the following order: `easting`,
44+
`northing`, `upward`.
45+
yaw : float, optional
46+
Rotation angle about the upward axis, in degrees.
47+
pitch : float, optional
48+
Rotation angle about the northing axis (after yaw rotation), in degrees.
49+
A positive pitch angle *lifts* the side of the ellipsoid pointing in easting
50+
direction.
51+
roll : float, optional
52+
Rotation angle about the easting axis (after yaw and pitch rotation), in
53+
degrees.
54+
**kwargs : dict
55+
Extra arguments passed to the ellipsoid classes. They can be physical properties
56+
like ``density``, ``susceptibility``, and ``remanent_mag``.
57+
58+
Returns
59+
-------
60+
ellipsoid : harmonica.typing.Ellipsoid
61+
Instance of one of the ellipsoid classes: :class:`harmonica.OblateEllipsoid`,
62+
:class:`harmonica.ProlateEllipsoid`, :class:`harmonica.Sphere`, or
63+
:class:`harmonica.TriaxialEllipsoid`.
64+
65+
Examples
66+
--------
67+
Define a prolate, oblate, triaxial or spherical ellipsoid by passing its semiaxes in
68+
any order:
69+
70+
>>> ellipsoid = create_ellipsoid(1, 1, 2, center=(1., 2., 3.))
71+
>>> print(type(ellipsoid).__name__)
72+
ProlateEllipsoid
73+
>>> ellipsoid.a
74+
2
75+
>>> ellipsoid.b
76+
1
77+
>>> ellipsoid.c
78+
1
79+
80+
>>> ellipsoid = create_ellipsoid(1, 2, 2, center=(1., 2., 3.))
81+
>>> print(type(ellipsoid).__name__)
82+
OblateEllipsoid
83+
>>> ellipsoid.a
84+
1
85+
>>> ellipsoid.b
86+
2
87+
>>> ellipsoid.c
88+
2
89+
90+
>>> ellipsoid = create_ellipsoid(1, 2, 3, center=(1., 2., 3.))
91+
>>> print(type(ellipsoid).__name__)
92+
TriaxialEllipsoid
93+
>>> ellipsoid.a
94+
3
95+
>>> ellipsoid.b
96+
2
97+
>>> ellipsoid.c
98+
1
99+
100+
We can specify rotation angles while creating the ellipsoid:
101+
102+
>>> ellipsoid = create_ellipsoid(1, 1, 2, yaw=85, pitch=32, center=(1., 2., 3.))
103+
>>> ellipsoid.yaw
104+
85
105+
>>> ellipsoid.pitch
106+
32
107+
108+
The function can also take physical properties:
109+
110+
>>> density, susceptibility = -200.0, 0.4
111+
>>> ellipsoid = create_ellipsoid(
112+
... 1, 1, 2, center=(1., 2., 3.),
113+
... density=density,
114+
... susceptibility=susceptibility,
115+
... )
116+
"""
117+
# Sanity checks
118+
for semiaxis, value in zip(("a", "b", "c"), (a, b, c), strict=True):
119+
if value <= 0:
120+
msg = (
121+
f"Invalid value of '{semiaxis}' equal to '{value}'. "
122+
"It must be positive."
123+
)
124+
raise ValueError(msg)
125+
126+
if a == b == c:
127+
return Sphere(a, center=center, **kwargs)
128+
129+
# Sort semiaxes so a >= b >= c
130+
c, b, a = sorted((a, b, c))
131+
132+
if a == b:
133+
a = c # set `a` as the smallest semiaxis (`c`)
134+
ellipsoid = OblateEllipsoid(a, b, yaw=yaw, pitch=pitch, center=center, **kwargs)
135+
elif b == c:
136+
ellipsoid = ProlateEllipsoid(
137+
a, b, yaw=yaw, pitch=pitch, center=center, **kwargs
138+
)
139+
else:
140+
ellipsoid = TriaxialEllipsoid(
141+
a, b, c, yaw=yaw, pitch=pitch, roll=roll, center=center, **kwargs
142+
)
143+
144+
return ellipsoid
18145

19146

20147
class BaseEllipsoid:
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
import numpy.typing as npt
1616
from choclo.constants import GRAVITATIONAL_CONST
1717

18-
from ..errors import NoPhysicalPropertyWarning
19-
from ..typing import Coordinates, Ellipsoid
20-
from .utils_ellipsoids import calculate_lambda, get_elliptical_integrals, is_internal
18+
from ...errors import NoPhysicalPropertyWarning
19+
from ...typing import Coordinates, Ellipsoid
20+
from .utils import calculate_lambda, get_elliptical_integrals, is_internal
2121

2222

2323
def ellipsoid_gravity(
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
from scipy.constants import mu_0
1919
from scipy.special import ellipeinc, ellipkinc
2020

21-
from .._utils import magnetic_angles_to_vec
22-
from ..errors import NoPhysicalPropertyWarning
23-
from ..typing import Coordinates, Ellipsoid
24-
from .utils_ellipsoids import (
21+
from ..._utils import magnetic_angles_to_vec
22+
from ...errors import NoPhysicalPropertyWarning
23+
from ...typing import Coordinates, Ellipsoid
24+
from .utils import (
2525
calculate_lambda,
2626
get_derivatives_of_elliptical_integrals,
2727
get_elliptical_integrals,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (c) 2018 The Harmonica Developers.
2+
# Distributed under the terms of the BSD 3-Clause License.
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
#
5+
# This code is part of the Fatiando a Terra project (https://www.fatiando.org)
6+
#

harmonica/tests/test_ellipsoids.py renamed to harmonica/tests/ellipsoids/test_ellipsoids.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@
88
Test ellipsoid classes.
99
"""
1010

11+
import itertools
1112
import re
1213

1314
import numpy as np
1415
import pytest
1516

16-
from harmonica import OblateEllipsoid, ProlateEllipsoid, Sphere, TriaxialEllipsoid
17+
from harmonica import (
18+
OblateEllipsoid,
19+
ProlateEllipsoid,
20+
Sphere,
21+
TriaxialEllipsoid,
22+
create_ellipsoid,
23+
)
1724

1825

1926
class TestProlateEllipsoid:
@@ -434,3 +441,106 @@ def test_invalid_remanent_mag_shape(self, ellipsoid_class, ellipsoid_args):
434441
msg = re.escape("Invalid 'remanent_mag' with shape '(2,)'")
435442
with pytest.raises(ValueError, match=msg):
436443
ellipsoid_class(**ellipsoid_args, remanent_mag=remanent_mag)
444+
445+
446+
class TestCreateEllipsoid:
447+
"""
448+
Test the ``create_ellipsoid`` function.
449+
"""
450+
451+
def test_sphere(self):
452+
"""Test ``create_ellipsoid`` when expecting a ``Sphere``."""
453+
a = 50
454+
center = (1.0, 2.0, 3.0)
455+
sphere = create_ellipsoid(a, a, a, center=center)
456+
assert isinstance(sphere, Sphere)
457+
assert sphere.a == a
458+
np.testing.assert_almost_equal(sphere.center, center)
459+
460+
@pytest.mark.parametrize(
461+
("a", "b", "c"), [(2.0, 1.0, 1.0), (1.0, 2.0, 1.0), (1.0, 1.0, 2.0)]
462+
)
463+
def test_prolate(self, a, b, c):
464+
"""Test ``create_ellipsoid`` when expecting a ``ProlateEllipsoid``."""
465+
center = (1.0, 2.0, 3.0)
466+
yaw, pitch = 30.0, -17.0
467+
ellipsoid = create_ellipsoid(a, b, c, center=center, yaw=yaw, pitch=pitch)
468+
assert isinstance(ellipsoid, ProlateEllipsoid)
469+
assert ellipsoid.a == 2.0
470+
assert ellipsoid.b == 1.0
471+
assert ellipsoid.c == 1.0
472+
assert ellipsoid.yaw == yaw
473+
assert ellipsoid.pitch == pitch
474+
np.testing.assert_almost_equal(ellipsoid.center, center)
475+
476+
@pytest.mark.parametrize(
477+
("a", "b", "c"), [(2.0, 3.0, 3.0), (3.0, 2.0, 3.0), (3.0, 3.0, 2.0)]
478+
)
479+
def test_oblate(self, a, b, c):
480+
"""Test ``create_ellipsoid`` when expecting a ``OblateEllipsoid``."""
481+
center = (1.0, 2.0, 3.0)
482+
yaw, pitch = 30.0, -17.0
483+
ellipsoid = create_ellipsoid(a, b, c, center=center, yaw=yaw, pitch=pitch)
484+
assert isinstance(ellipsoid, OblateEllipsoid)
485+
assert ellipsoid.a == 2.0
486+
assert ellipsoid.b == 3.0
487+
assert ellipsoid.c == 3.0
488+
assert ellipsoid.yaw == yaw
489+
assert ellipsoid.pitch == pitch
490+
np.testing.assert_almost_equal(ellipsoid.center, center)
491+
492+
@pytest.mark.parametrize(("a", "b", "c"), itertools.permutations((1.0, 2.0, 3.0)))
493+
def test_triaxial(self, a, b, c):
494+
"""Test ``create_ellipsoid`` when expecting a ``TriaxialEllipsoid``."""
495+
center = (1.0, 2.0, 3.0)
496+
yaw, pitch, roll = 30.0, -17.0, 45.0
497+
ellipsoid = create_ellipsoid(
498+
a, b, c, center=center, yaw=yaw, pitch=pitch, roll=roll
499+
)
500+
assert isinstance(ellipsoid, TriaxialEllipsoid)
501+
assert ellipsoid.a == 3.0
502+
assert ellipsoid.b == 2.0
503+
assert ellipsoid.c == 1.0
504+
assert ellipsoid.yaw == yaw
505+
assert ellipsoid.pitch == pitch
506+
assert ellipsoid.roll == roll
507+
np.testing.assert_almost_equal(ellipsoid.center, center)
508+
509+
@pytest.mark.parametrize(
510+
("a", "b", "c"),
511+
[(-1.0, 2.0, 3.0), (1.0, -2.0, 3.0), (1.0, 2.0, -3.0), (0.0, 1.0, 2.0)],
512+
ids=["a", "b", "c", "a-zero"],
513+
)
514+
def test_invalid_semiaxes(self, a, b, c):
515+
"""Test error when semiaxes are non-positive."""
516+
msg = (
517+
r"Invalid value of '[abc]' equal to '-?[0-9]+\.[0-9]+'\. "
518+
r"It must be positive\."
519+
)
520+
with pytest.raises(ValueError, match=msg):
521+
create_ellipsoid(a, b, c, center=(0, 1, 2))
522+
523+
@pytest.mark.parametrize(
524+
("a", "b", "c"),
525+
[(1.0, 1.0, 1.0), (1.0, 1.0, 2.0), (2.0, 2.0, 1.0), (1.0, 2.0, 3.0)],
526+
ids=["sphere", "prolate", "oblate", "triaxial"],
527+
)
528+
@pytest.mark.parametrize(
529+
"physical_property",
530+
["density", "susceptibility", "remanent_mag"],
531+
)
532+
def test_physical_properties(self, a, b, c, physical_property):
533+
"""Test if physical properties are present in the objects."""
534+
match physical_property:
535+
case "density":
536+
value = 200.0
537+
case "susceptibility":
538+
value = 0.1
539+
case "remanent_mag":
540+
value = np.array([1.0, 2.0, 3.0])
541+
case _:
542+
raise ValueError()
543+
kwargs = {physical_property: value}
544+
center = (1.0, 2.0, 3.0)
545+
ellipsoid = create_ellipsoid(a, b, c, center=center, **kwargs)
546+
np.testing.assert_allclose(getattr(ellipsoid, physical_property), value)

harmonica/tests/test_ellipsoid_gravity.py renamed to harmonica/tests/ellipsoids/test_gravity.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,14 @@
2121
import pytest
2222
import verde as vd
2323

24-
from harmonica import point_gravity
25-
from harmonica.errors import NoPhysicalPropertyWarning
26-
27-
from .._forward.ellipsoid_gravity import (
28-
ellipsoid_gravity,
29-
)
30-
from .._forward.ellipsoids import (
24+
from harmonica import ellipsoid_gravity, point_gravity
25+
from harmonica._forward.ellipsoids import (
3126
OblateEllipsoid,
3227
ProlateEllipsoid,
3328
Sphere,
3429
TriaxialEllipsoid,
3530
)
31+
from harmonica.errors import NoPhysicalPropertyWarning
3632

3733

3834
def build_ellipsoid(ellipsoid_type, *, center=(0, 0, 0), density=None):

0 commit comments

Comments
 (0)