Files
skyview.astronomiemuseum.de/public/py/test_observation_suggestions_api.py
T
2026-09-15 15:58:51 +02:00

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()