Skip to content

Commit 7cc3bfd

Browse files
committed
Update syndyne tests for full coverage and more control
Replace random_syn* with fake_syn*
1 parent 2280a54 commit 7cc3bfd

1 file changed

Lines changed: 257 additions & 79 deletions

File tree

sbpy/dynamics/tests/test_syndynes.py

Lines changed: 257 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,111 @@
55
import astropy.units as u
66
from astropy.coordinates.errors import ConvertError
77
from astropy.time import Time
8-
from ..syndynes import Syndyne, Synchrone, Syndynes, Synchrones, SynGenerator
8+
from astropy.wcs import WCS
9+
from ..syndynes import (
10+
Syndyne,
11+
Syndynes,
12+
Synchrone,
13+
Synchrones,
14+
SourceOrbit,
15+
SynCollection,
16+
SynGenerator,
17+
)
918
from ..state import State
1019
from ..models import SolarGravity, SolarGravityAndRadiationPressure
1120

1221

1322
pytest.importorskip("scipy")
1423

1524

25+
def fake_state(source, betas, ages):
26+
"""Fake dynamics: particles are displaced beta * 1e4 km from the source for
27+
a little more control on the tests."""
28+
r0 = 10000 * u.km
29+
v0 = 0.1 * u.km / u.s
30+
length = len(betas)
31+
return State(
32+
(betas * r0)[:, np.newaxis] + source.r,
33+
(betas * v0)[:, np.newaxis] + source.v,
34+
source.t - ages,
35+
frame=source.frame,
36+
)
37+
38+
39+
def fake_syndynes():
40+
observer = State(
41+
[0, 1, 0] * u.au,
42+
[30, 0, 0] * u.km / u.s,
43+
Time("2025-12-29"),
44+
frame="icrs",
45+
)
46+
comet = State(
47+
[1, 0, 0] * u.au,
48+
[0, 30, 0] * u.km / u.s,
49+
observer.t,
50+
frame="heliocentriceclipticiau76",
51+
)
52+
53+
betas = 10.0 ** -np.arange(4)
54+
ages = np.arange(10) * u.day
55+
56+
items = []
57+
for beta in betas:
58+
particles = fake_state(comet, np.repeat(beta, 10), ages * 0)
59+
initial = fake_state(comet, np.zeros(10), ages)
60+
items.append(
61+
Syndyne(
62+
comet,
63+
beta,
64+
ages,
65+
particles.r,
66+
particles.v,
67+
particles.t,
68+
initial,
69+
observer=observer,
70+
)
71+
)
72+
73+
return comet, observer, betas, ages, items
74+
75+
76+
def fake_synchrones():
77+
observer = State(
78+
[0, 1, 0] * u.au,
79+
[30, 0, 0] * u.km / u.s,
80+
Time("2025-12-29"),
81+
frame="icrs",
82+
)
83+
comet = State(
84+
[1, 0, 0] * u.au,
85+
[0, 30, 0] * u.km / u.s,
86+
observer.t,
87+
frame="heliocentriceclipticiau76",
88+
)
89+
90+
betas = 10.0 ** -np.arange(4)
91+
ages = np.arange(10) * u.day
92+
93+
items = []
94+
for age in ages:
95+
particles = fake_state(comet, betas, age)
96+
initial = fake_state(comet, np.zeros(10), ages)
97+
items.append(
98+
Synchrone(
99+
comet,
100+
betas,
101+
age,
102+
particles.r,
103+
particles.v,
104+
particles.t,
105+
initial,
106+
observer=observer,
107+
)
108+
)
109+
110+
return comet, observer, betas, ages, items
111+
112+
16113
@pytest.fixture
17114
def example_syndynes():
18115
comet = State(
@@ -115,8 +212,6 @@ def test_to_ephem(self, example_syndynes):
115212
assert u.allclose(eph["deltadot"], deltadot, atol=1e-12 * u.km / u.s)
116213
assert u.allclose(eph["lon"], coords.lon)
117214
assert u.allclose(eph["lat"], coords.lat)
118-
assert u.allclose(eph["coords"].lon, coords.lon)
119-
assert u.allclose(eph["coords"].lat, coords.lat)
120215
assert u.allclose(eph["x"], particles.x)
121216
assert u.allclose(eph["y"], particles.y)
122217
assert u.allclose(eph["z"], particles.z)
@@ -161,10 +256,7 @@ def test_to_ephem(self, example_syndynes):
161256
assert np.all(eph["t_relative"] == 0 * u.s)
162257
assert eph.meta["observer"] is None
163258
assert np.all(
164-
[
165-
k not in eph
166-
for k in ("delta", "deltadot", "ra", "dec", "lon", "lat", "coords")
167-
]
259+
[k not in eph for k in ("delta", "deltadot", "ra", "dec", "lon", "lat")]
168260
)
169261

170262
def test_init_1d(self, example_syndynes):
@@ -219,6 +311,24 @@ def test_getitem(self, example_syndynes):
219311
assert isinstance(dust.syndynes()[:1], Syndynes)
220312

221313

314+
class TestSynCollection:
315+
def test_plot(self):
316+
"""Test plot's label_format when plotting a mix of syndynes and synchrones"""
317+
318+
plt = pytest.importorskip("matplotlib.pyplot")
319+
320+
_, _, _, _, syndynes = fake_syndynes()
321+
_, _, _, _, synchrones = fake_synchrones()
322+
collection = SynCollection(syndynes + synchrones)
323+
324+
_, ax = plt.subplots()
325+
collection.plot()
326+
327+
for i, line in enumerate(ax.get_lines()):
328+
# when label="", mpl sets it to _child0, _child1, ...
329+
assert line.get_label().startswith("_child")
330+
331+
222332
class TestSynchrone:
223333
def test_init(self, example_syndynes):
224334
comet, betas, ages, dust, observer = example_syndynes
@@ -287,8 +397,8 @@ def test_to_ephem(self, example_syndynes):
287397

288398
eph = synchrone.to_ephem()
289399

290-
# most everything is tested in TestSyndyne.test_to_ephem
291-
# we just need to make sure the betas and ages are right
400+
# most everything is tested in TestSyndyne.test_to_ephem let's just
401+
# check the betas and ages
292402
assert np.allclose(eph["beta_rad"], betas)
293403
assert u.allclose(eph["age"], age)
294404

@@ -299,90 +409,158 @@ def test_getitem(self, example_syndynes):
299409
assert isinstance(dust.synchrones()[:1], Synchrones)
300410

301411

302-
def random_state(length=1):
303-
return State(
304-
np.random.rand(length, 3).squeeze() * u.au,
305-
np.random.rand(length, 3).squeeze() * u.km / u.s,
306-
np.random.rand() * u.day,
307-
)
412+
class TestSyndynes:
413+
def test_init(self):
414+
comet, _, betas, ages, items = fake_syndynes()
308415

416+
# create from list
417+
syndynes = Syndynes(items)
309418

310-
def test_syndynes():
311-
comet = random_state()
312-
betas = np.linspace(0, 1, 3)
313-
ages = np.arange(10) * u.day
419+
# create from syndynes object
420+
syndynes2 = Syndynes(syndynes)
421+
assert np.all(syndynes2[0].r == syndynes[0].r)
314422

315-
items = []
316-
for beta in betas:
317-
particles = random_state(len(ages))
318-
initial = random_state(len(ages))
319-
items.append(
320-
Syndyne(
321-
comet,
322-
beta,
323-
ages,
324-
particles.r,
325-
particles.v,
326-
particles.t,
327-
initial,
328-
)
423+
# create from tuple
424+
syndynes3 = Syndynes(tuple(items))
425+
assert np.all(syndynes3[0].r == syndynes[0].r)
426+
427+
# cannot mix syndynes and synchrones
428+
_, _, _, _, items2 = fake_synchrones()
429+
with pytest.raises(TypeError):
430+
Syndynes(items + items2)
431+
432+
# test empty syndynes objects
433+
assert len(Syndynes([])) == 0
434+
435+
def test_getitem(self):
436+
_, _, _, _, items = fake_syndynes()
437+
syndynes = Syndynes(items)
438+
439+
# get a single Syndyne
440+
assert isinstance(syndynes[2], Syndyne)
441+
assert syndynes[2] is items[2]
442+
443+
# get Syndynes from a slice
444+
assert len(syndynes[:3]) == 3
445+
assert isinstance(syndynes[:3], Syndynes)
446+
447+
# get Syndynes from a tuple
448+
assert isinstance(syndynes[(0, 1)], Syndynes)
449+
450+
def test_repr(self):
451+
_, _, _, _, items = fake_syndynes()
452+
syndynes = Syndynes(items)
453+
assert repr(syndynes) == "<Syndynes: betas=[1. 0.1 0.01 0.001]>"
454+
455+
def test_to_ephem(self):
456+
comet, _, _, _, items = fake_syndynes()
457+
syndynes = Syndynes(items)
458+
459+
eph = syndynes.to_ephem()
460+
assert len(eph) == 40
461+
assert all(eph.meta["source"]["r"] == comet.r)
462+
463+
# [1] is 2nd index of 1st syndyne
464+
assert eph["x"][1] == items[0].x[1]
465+
466+
# [13] is 4th index of 2nd syndyne
467+
assert eph["vz"][13] == items[1].v_z[3]
468+
469+
assert len(Syndynes([]).to_ephem()) == 0
470+
471+
def test_plot(self):
472+
plt = pytest.importorskip("matplotlib.pyplot")
473+
474+
comet, observer, betas, ages, items = fake_syndynes()
475+
syndynes = Syndynes(items)
476+
477+
_, ax = plt.subplots()
478+
syndynes.plot()
479+
480+
# observer is sqrt(2) au away, particles are up to 1e-4 au = 14959 km =
481+
# 15"
482+
for i, line in enumerate(ax.get_lines()):
483+
assert line.get_label() == f"$\\beta={betas[i]}$"
484+
assert all(line.get_xdata() <= 15 * betas[i] * u.arcsec)
485+
assert all(line.get_ydata() <= 15 * betas[i] * u.arcsec)
486+
487+
coords = observer.observe(comet)
488+
489+
# 1 deg/pix
490+
wcs = WCS()
491+
wcs.wcs.ctype = "RA---TAN", "DEC--TAN"
492+
wcs.wcs.crval = coords.ra.deg, coords.dec.deg
493+
494+
syndynes.plot(wcs=wcs)
495+
496+
# 15" / (1 deg/pix) = 0.00417 pix
497+
for i, line in enumerate(ax.get_lines()[len(syndynes) :]):
498+
assert all(line.get_xdata() <= 4.17e-3 * betas[i])
499+
assert all(line.get_ydata() <= 4.17e-3 * betas[i])
500+
501+
# cannot plot relative to source when observer is None
502+
state = fake_state(comet, [1], ages)
503+
syndyne = Syndyne(
504+
comet, [1], ages, state.r, state.v, state.t, comet, observer=None
329505
)
506+
with pytest.raises(ValueError):
507+
syndyne.plot()
330508

331-
syndynes = Syndynes(items)
332-
assert syndynes[2] is items[2]
333-
assert len(syndynes[:3]) == 3
334-
assert repr(syndynes) == "<Syndynes: betas=[0. 0.5 1. ]>"
335509

336-
eph = syndynes.to_ephem()
337-
assert len(eph) == 30
338-
assert all(eph.meta["source"]["r"] == comet.r)
510+
class TestSynchrones:
511+
# most of the relevant tests are covered by TestSyndynes
339512

340-
syndynes2 = Syndynes(syndynes)
341-
assert np.all(syndynes2[0].r == syndynes[0].r)
342-
syndynes3 = Syndynes(tuple(items))
343-
assert np.all(syndynes3[0].r == syndynes[0].r)
513+
def test_init(self):
514+
_, _, _, _, items = fake_synchrones()
515+
synchrones = Synchrones(items)
344516

345-
particles = random_state(len(betas))
346-
synchrone = Synchrones(
347-
[
348-
Synchrone(
349-
comet, betas, ages[0], particles.r, particles.v, particles.t, particles
350-
)
351-
]
352-
)
353-
with pytest.raises(TypeError):
354-
Syndynes(items + [synchrone])
517+
assert synchrones[2] is items[2]
518+
assert len(synchrones) == len(items)
355519

356-
# test emptpy syndynes objects
357-
assert len(Syndynes([])) == 0
358-
assert len(Syndynes([]).to_ephem()) == 0
520+
def test_getitem(self):
521+
_, _, _, _, items = fake_synchrones()
522+
synchrones = Synchrones(items)
359523

524+
# index with integer returns one synchrone
525+
assert synchrones[2] is items[2]
526+
assert isinstance(synchrones[0], Synchrone)
360527

361-
def test_synchrones():
362-
comet = random_state()
363-
betas = np.linspace(0, 1, 3)
364-
ages = np.arange(10) * u.day
528+
# index with slice returns synchrone collection
529+
assert isinstance(synchrones[:3], Synchrones)
365530

366-
items = []
367-
for age in ages:
368-
particles = random_state(len(ages))
369-
initial = random_state(len(ages))
370-
items.append(
371-
Synchrone(
372-
comet,
373-
betas,
374-
age,
375-
particles.r,
376-
particles.v,
377-
particles.t,
378-
initial,
379-
)
531+
# index with tuple returns synchrone collection
532+
assert isinstance(synchrones[(0, 1)], Synchrones)
533+
534+
def test_repr(self):
535+
_, _, _, _, items = fake_synchrones()
536+
synchrones = Synchrones(items)
537+
assert (
538+
repr(synchrones) == "<Synchrones: ages=[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.] d>"
380539
)
381540

382-
synchrones = Synchrones(items)
383-
assert synchrones[2] is items[2]
384-
assert len(synchrones[:3]) == 3
385-
assert repr(synchrones) == "<Synchrones: ages=[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.] d>"
541+
def test_plot(self):
542+
plt = pytest.importorskip("matplotlib.pyplot")
543+
544+
comet, observer, betas, ages, items = fake_synchrones()
545+
synchrones = Synchrones(items)
546+
547+
_, ax = plt.subplots()
548+
synchrones.plot()
549+
550+
for i, line in enumerate(ax.get_lines()):
551+
assert line.get_label() == f"$\\Delta t={ages[i]}$"
552+
553+
554+
class TestSourceOrbit:
555+
def test_dt(self):
556+
comet = State([1, 0, 0] * u.au, [0, 30, 0] * u.km / u.s, Time("2023-12-07"))
557+
dt = [-1, 0, 1] * u.day
558+
ages = -dt
559+
state = fake_state(comet, [0, 1, 2], ages)
560+
orbit = SourceOrbit(comet, dt, state.r, state.v, comet.t + dt)
561+
562+
assert all(orbit.dt == dt)
563+
assert all(orbit.epoch == comet.t + dt)
386564

387565

388566
class TestSynGenerator:

0 commit comments

Comments
 (0)