131 lines
6.5 KiB
Python
131 lines
6.5 KiB
Python
"""Run: python -m unittest discover -s public/py -p test_observation_suggestions_api.py"""
|
|
import unittest
|
|
|
|
import astronomy
|
|
import observation_suggestions_api as api
|
|
|
|
|
|
def payload():
|
|
return {'location': {'latitude': 50.3774, 'longitude': 11.1899, 'elevation': 640, 'timezone': 'Europe/Berlin'},
|
|
'filters': {'observation_date': '2026-09-15', 'object_types': ['moon', 'planets', 'galaxies'],
|
|
'night_halves': ['first', 'second'], 'object_size_min': '', 'object_size_max': '',
|
|
'object_magnitude': '12', 'object_altitude_min': '20', 'object_altitude_max': '90',
|
|
'object_azimuth_start': '0', 'object_azimuth_end': '360', 'sun_altitude_max': '-12',
|
|
'moon_separation_min': '', 'include_unknown_magnitude': ''},
|
|
'objects': [{'id': 'm31', 'name': 'M31', 'group': 'galaxies', 'ra_deg': 10.6847,
|
|
'dec_deg': 41.269, 'magnitude': 3.44, 'size_arcsec': 10680}]}
|
|
|
|
|
|
class ObservationTests(unittest.TestCase):
|
|
def test_azimuth_wrap_and_full_circle(self):
|
|
for value in (0, 20, 60, 300, 350):
|
|
self.assertTrue(api.azimuth_matches(value, 300, 60))
|
|
self.assertFalse(api.azimuth_matches(180, 300, 60))
|
|
for value in (0, 90, 180, 359.9):
|
|
self.assertTrue(api.azimuth_matches(value, 0, 360))
|
|
self.assertFalse(api.azimuth_matches(0, 90, 270))
|
|
|
|
def test_size_exempts_non_deep_sky(self):
|
|
f = api.validate(payload())[0]
|
|
f.update(object_size_min=60, object_size_max=600)
|
|
for group in ('moon', 'planets', 'double_stars', 'variable_stars', 'comets'):
|
|
self.assertTrue(api.size_matches(None, group, f))
|
|
self.assertFalse(api.size_matches(None, 'galaxies', f))
|
|
self.assertFalse(api.size_matches(601, 'galaxies', f))
|
|
self.assertTrue(api.size_matches(60, 'galaxies', f))
|
|
|
|
def test_magnitude_direction_unknown_and_zero(self):
|
|
f = api.validate(payload())[0]
|
|
self.assertTrue(api.magnitude_matches(-2, f))
|
|
self.assertTrue(api.magnitude_matches(12, f))
|
|
self.assertFalse(api.magnitude_matches(12.1, f))
|
|
self.assertFalse(api.magnitude_matches(None, f))
|
|
f['unknown'] = True
|
|
self.assertTrue(api.magnitude_matches(None, f))
|
|
f['object_magnitude'] = 0
|
|
self.assertFalse(api.magnitude_matches(1, f))
|
|
|
|
def test_invalid_input_rejected(self):
|
|
for key, value in [('object_altitude_min', '91'), ('sun_altitude_max', 'NaN'),
|
|
('object_size_min', '-1'), ('observation_date', '2026-02-30'),
|
|
('object_types', []), ('night_halves', [])]:
|
|
p = payload()
|
|
p['filters'][key] = value
|
|
with self.subTest(key=key), self.assertRaises(ValueError):
|
|
api.validate(p)
|
|
p = payload()
|
|
p['filters'].update(object_size_min='600', object_size_max='60')
|
|
with self.assertRaises(ValueError):
|
|
api.validate(p)
|
|
|
|
def test_frame_matches_library_horizon(self):
|
|
_, observer, _, start, _ = api.validate(payload())
|
|
t = api.astro_time(start)
|
|
rotation = astronomy.Rotation_EQJ_HOR(t, observer).rot
|
|
for body in (astronomy.Body.Moon, astronomy.Body.Mars, astronomy.Body.Sun):
|
|
j2000 = astronomy.Equator(body, t, observer, False, True)
|
|
ofdate = astronomy.Equator(body, t, observer, True, True)
|
|
expected = astronomy.Horizon(t, observer, ofdate.ra, ofdate.dec, astronomy.Refraction.Airless)
|
|
altitude, azimuth = api.horizontal(api.xyz(j2000.vec), rotation)
|
|
self.assertAlmostEqual(altitude, expected.altitude, places=7)
|
|
self.assertAlmostEqual(azimuth, expected.azimuth, places=7)
|
|
|
|
def test_night_bounds_and_half_split(self):
|
|
f, observer, _, start, end = api.validate(payload())
|
|
windows = api.night_windows(start, end, observer, f['sun_altitude_max'])
|
|
self.assertEqual(len(windows), 1)
|
|
a, b = windows[0]
|
|
for instant in (a, b):
|
|
self.assertLessEqual(api.sun_altitude(instant, observer), -12)
|
|
self.assertAlmostEqual(api.sun_altitude(instant, observer), -12, delta=0.01)
|
|
first = api.selected_windows(windows, ['first'])[0]
|
|
second = api.selected_windows(windows, ['second'])[0]
|
|
self.assertEqual(first[1], second[0])
|
|
self.assertEqual(first[1] - first[0], second[1] - second[0])
|
|
|
|
def test_dst_night_duration(self):
|
|
for date, hours in [('2026-03-28', 23), ('2026-10-24', 25)]:
|
|
p = payload()
|
|
p['filters']['observation_date'] = date
|
|
_, _, _, a, b = api.validate(p)
|
|
self.assertEqual((b - a).total_seconds() / 3600, hours)
|
|
|
|
def test_polar_day_empty_and_polar_night_explained(self):
|
|
p = payload()
|
|
p['location'].update(latitude=80, longitude=15, timezone='Arctic/Longyearbyen')
|
|
p['filters']['observation_date'] = '2026-06-21'
|
|
result = api.calculate(p)
|
|
self.assertEqual(result['rows'], [])
|
|
self.assertTrue(result['message'])
|
|
p['filters']['observation_date'] = '2026-12-21'
|
|
result = api.calculate(p)
|
|
self.assertTrue(any('12 Uhr' in warning for warning in result['warnings']))
|
|
|
|
def test_moon_separation_exemption_and_below_horizon(self):
|
|
f = api.validate(payload())[0]
|
|
f['moon_separation_min'] = 60
|
|
frame = {'rotation': [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'moon': (0, 0, 1), 'moon_alt': 90}
|
|
self.assertIsNone(api.sample_matches((0, 0, 1), 5, {'group': 'galaxies'}, frame, f))
|
|
self.assertIsNotNone(api.sample_matches((0, 0, 1), 5, {'group': 'moon'}, frame, f))
|
|
frame['moon_alt'] = -5
|
|
self.assertIsNotNone(api.sample_matches((0, 0, 1), 5, {'group': 'galaxies'}, frame, f))
|
|
|
|
def test_real_night_returns_m31_and_matching_positions(self):
|
|
p = payload()
|
|
result = api.calculate(p)
|
|
self.assertTrue(result['ok'])
|
|
self.assertIn('m31', [row['id'] for row in result['rows']])
|
|
for row in result['rows']:
|
|
self.assertGreaterEqual(row['best']['altitude'], 20)
|
|
self.assertLessEqual(row['best']['magnitude'], 12)
|
|
self.assertTrue(row['intervals'])
|
|
p['filters'].update(object_size_max='1')
|
|
small = api.calculate(p)
|
|
self.assertNotIn('m31', [row['id'] for row in small['rows']])
|
|
self.assertEqual({r['id'] for r in result['rows'] if r['group'] in ('moon', 'planets')},
|
|
{r['id'] for r in small['rows'] if r['group'] in ('moon', 'planets')})
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|