def test_straight_overhead(): """ With a precise CIRS<->AltAz transformation this should give Alt=90 exactly If the CIRS self-transform breaks it won't, due to improper treatment of aberration """ t = Time('J2010') obj = EarthLocation(-1 * u.deg, 52 * u.deg, height=10. * u.km) home = EarthLocation(-1 * u.deg, 52 * u.deg, height=0. * u.km) # An object that appears straight overhead - FOR A GEOCENTRIC OBSERVER. # Note, this won't be overhead for a topocentric observer because of # aberration. cirs_geo = obj.get_itrs(t).transform_to(CIRS(obstime=t)) # now get the Geocentric CIRS position of observatory obsrepr = home.get_itrs(t).transform_to(CIRS(obstime=t)).cartesian # topocentric CIRS position of a straight overhead object cirs_repr = cirs_geo.cartesian - obsrepr # create a CIRS object that appears straight overhead for a TOPOCENTRIC OBSERVER topocentric_cirs_frame = CIRS(obstime=t, location=home) cirs_topo = topocentric_cirs_frame.realize_frame(cirs_repr) # Check AltAz (though Azimuth can be anything so is not tested). aa = cirs_topo.transform_to(AltAz(obstime=t, location=home)) assert_allclose(aa.alt, 90 * u.deg, atol=1 * u.uas, rtol=0) # Check HADec. hd = cirs_topo.transform_to(HADec(obstime=t, location=home)) assert_allclose(hd.ha, 0 * u.hourangle, atol=1 * u.uas, rtol=0) assert_allclose(hd.dec, 52 * u.deg, atol=1 * u.uas, rtol=0)
def test_regression_simple_5133(): """ Simple test to check if alt-az calculations respect height of observer Because ITRS is geocentric and includes aberration, an object that appears 'straight up' to a geocentric observer (ITRS) won't be straight up to a topocentric observer - see https://github.com/astropy/astropy/issues/10983 This is why we construct a topocentric GCRS SkyCoord before calculating AltAz """ t = Time('J2010') obj = EarthLocation(-1 * u.deg, 52 * u.deg, height=[10., 0.] * u.km) home = EarthLocation(-1 * u.deg, 52 * u.deg, height=5. * u.km) obsloc_gcrs, obsvel_gcrs = home.get_gcrs_posvel(t) gcrs_geo = obj.get_itrs(t).transform_to(GCRS(obstime=t)) obsrepr = home.get_itrs(t).transform_to(GCRS(obstime=t)).cartesian topo_gcrs_repr = gcrs_geo.cartesian - obsrepr topocentric_gcrs_frame = GCRS(obstime=t, obsgeoloc=obsloc_gcrs, obsgeovel=obsvel_gcrs) gcrs_topo = topocentric_gcrs_frame.realize_frame(topo_gcrs_repr) aa = gcrs_topo.transform_to(AltAz(obstime=t, location=home)) # az is more-or-less undefined for straight up or down assert_quantity_allclose(aa.alt, [90, -90] * u.deg, rtol=1e-7) assert_quantity_allclose(aa.distance, 5 * u.km)
def iod_obs_tod(location: EarthLocation, epoch: Time): """ Get the true of date (tod) position-velocity vector of the observatory Parameters ---------- location: `~astropy.coordinates.EarthLocation` Position vector in body fixed reference frame. epoch: `~astropy.time.Time` Returns ------- pos, vel: ~astropy.units.Quantity Position and velocity vector of the observation, in tod frame """ from astropy.coordinates.earth import OMEGA_EARTH from astropy import _erfa as erfa assert isinstance(location, EarthLocation) assert isinstance(epoch, Time) dut = epoch.get_delta_ut1_utc(iers_table=iers_table) ut1 = Time(epoch.mjd + dut.to_value('day'), format='mjd', scale='ut1') earth_rotation_angle = erfa.era00(ut1.jd1, ut1.jd2) * u.rad loc = location.get_itrs() unit = loc.x.unit loc = np.array([loc.x.value, loc.y.value, loc.z.value]) pos = np.matmul(rotation_matrix(-earth_rotation_angle, 'z'), loc) vel = [-OMEGA_EARTH.value * pos[1], OMEGA_EARTH.value * pos[0], 0.0] acc = [-OMEGA_EARTH.value**2 * pos[0], -OMEGA_EARTH.value**2 * pos[1], 0.0] return pos * unit, \ vel * unit * OMEGA_EARTH.unit, \ acc * unit * OMEGA_EARTH.unit * OMEGA_EARTH.unit
def iod_obs_tod(location: EarthLocation, epoch: Time): """ Get the true of date (tod) position-velocity vector of the observatory Parameters ---------- location: `~astropy.coordinates.EarthLocation` Position vector in body fixed reference frame. epoch: `~astropy.time.Time` Returns ------- pos, vel: ~astropy.units.Quantity Position and velocity vector of the observation, in tod frame """ from astropy.coordinates.earth import OMEGA_EARTH import nessan_fa as fa assert isinstance(location, EarthLocation) assert isinstance(epoch, Time) gmst, gast = fa.greenwich_sidereal_time(epoch) loc = location.get_itrs() unit = loc.x.unit loc = np.array([loc.x.value, loc.y.value, loc.z.value]) pos = np.matmul(rotation_matrix(-gast, 'z'), loc) vel = [-OMEGA_EARTH.value * pos[1], OMEGA_EARTH.value * pos[0], 0.0] acc = [-OMEGA_EARTH.value**2 * pos[0], -OMEGA_EARTH.value**2 * pos[1], 0.0] return pos * unit, \ vel * unit * OMEGA_EARTH.unit, \ acc * unit * OMEGA_EARTH.unit * OMEGA_EARTH.unit
def test_regression_simple_5133(): t = Time('J2010') obj = EarthLocation(-1*u.deg, 52*u.deg, height=[100., 0.]*u.km) home = EarthLocation(-1*u.deg, 52*u.deg, height=10.*u.km) aa = obj.get_itrs(t).transform_to(AltAz(obstime=t, location=home)) # az is more-or-less undefined for straight up or down assert_quantity_allclose(aa.alt, [90, -90]*u.deg, rtol=1e-5) assert_quantity_allclose(aa.distance, [90, 10]*u.km)
def geoTopoVector(longitude, latitude, elevation, jd): loc = EarthLocation(longitude, latitude, elevation) time = Time(jd, scale='utc', format='jd') itrs = loc.get_itrs(obstime=time) gcrs = itrs.transform_to(GCRS(obstime=time)) r = gcrs.cartesian # convert from m to km x = r.x.value / 1000.0 y = r.y.value / 1000.0 z = r.z.value / 1000.0 return x, y, z
def make_map(self): # still in beta version, use with caution # ref : https://gist.github.com/hayesla/42596c72ab686171fe516f9ab43300e2 hdu = fits.open(self.fname) header = hdu[0].header data = np.squeeze(hdu[0].data) data = np.squeeze(hdu[0].data) obstime = Time(header['date-obs']) frequency = header['crval3'] * u.Hz reference_coord = SkyCoord(header['crval1'] * u.deg, header['crval2'] * u.deg, frame='gcrs', obstime=obstime, distance=sun_coord.earth_distance(obstime), equinox='J2000') lofar_loc = EarthLocation(lat=52.905329712 * u.deg, lon=6.867996528 * u.deg) # location of the center of LOFAR lofar_coord = SkyCoord(lofar_loc.get_itrs(Time(obstime))) reference_coord_arcsec = reference_coord.transform_to( frames.Helioprojective(observer=lofar_coord)) cdelt1 = (np.abs(header['cdelt1']) * u.deg).to(u.arcsec) cdelt2 = (np.abs(header['cdelt2']) * u.deg).to(u.arcsec) P1 = sun_coord.P(obstime) new_header = sunpy.map.make_fitswcs_header( data, reference_coord_arcsec, reference_pixel=u.Quantity( [header['crpix1'] - 1, header['crpix2'] - 1] * u.pixel), scale=u.Quantity([cdelt1, cdelt2] * u.arcsec / u.pix), rotation_angle=-P1, wavelength=frequency.to(u.MHz), observatory='LOFAR') lofar_map = sunpy.map.Map(data, new_header) lofar_map_rotate = lofar_map.rotate() bl = SkyCoord(-2500 * u.arcsec, -2500 * u.arcsec, frame=lofar_map_rotate.coordinate_frame) tr = SkyCoord(2500 * u.arcsec, 2500 * u.arcsec, frame=lofar_map_rotate.coordinate_frame) lofar_submap = lofar_map_rotate.submap(bl, tr) return lofar_submap
def setup(self): wht = EarthLocation(342.12*u.deg, 28.758333333333333*u.deg, 2327*u.m) self.obstime = Time("2013-02-02T23:00") self.wht_itrs = wht.get_itrs(obstime=self.obstime)
def _get_components_and_classes(self): # The aim of this function is to return whatever is needed for # world_axis_object_components and world_axis_object_classes. It's easier # to figure it out in one go and then return the values and let the # properties return part of it. # Since this method might get called quite a few times, we need to cache # it. We start off by defining a hash based on the attributes of the # WCS that matter here (we can't just use the WCS object as a hash since # it is mutable) wcs_hash = (self.naxis, list(self.wcs.ctype), list(self.wcs.cunit), self.wcs.radesys, self.wcs.specsys, self.wcs.equinox, self.wcs.dateobs, self.wcs.lng, self.wcs.lat) # If the cache is present, we need to check that the 'hash' matches. if getattr(self, '_components_and_classes_cache', None) is not None: cache = self._components_and_classes_cache if cache[0] == wcs_hash: return cache[1] else: self._components_and_classes_cache = None # Avoid circular imports by importing here from astropy.wcs.utils import wcs_to_celestial_frame from astropy.coordinates import SkyCoord, EarthLocation from astropy.time.formats import FITS_DEPRECATED_SCALES from astropy.time import Time, TimeDelta components = [None] * self.naxis classes = {} # Let's start off by checking whether the WCS has a pair of celestial # components if self.has_celestial: try: celestial_frame = wcs_to_celestial_frame(self) except ValueError: # Some WCSes, e.g. solar, can be recognized by WCSLIB as being # celestial but we don't necessarily have frames for them. celestial_frame = None else: kwargs = {} kwargs['frame'] = celestial_frame kwargs['unit'] = u.deg classes['celestial'] = (SkyCoord, (), kwargs) components[self.wcs.lng] = ('celestial', 0, 'spherical.lon.degree') components[self.wcs.lat] = ('celestial', 1, 'spherical.lat.degree') # Next, we check for spectral components if self.has_spectral: # Find index of spectral coordinate ispec = self.wcs.spec ctype = self.wcs.ctype[ispec][:4] ctype = ctype.upper() kwargs = {} # Determine observer location and velocity # TODO: determine how WCS standard would deal with observer on a # spacecraft far from earth. For now assume the obsgeo parameters, # if present, give the geocentric observer location. if np.isnan(self.wcs.obsgeo[0]): observer = None else: earth_location = EarthLocation(*self.wcs.obsgeo[:3], unit=u.m) obstime = Time(self.wcs.mjdobs, format='mjd', scale='utc', location=earth_location) observer_location = SkyCoord( earth_location.get_itrs(obstime=obstime)) if self.wcs.specsys in VELOCITY_FRAMES: frame = VELOCITY_FRAMES[self.wcs.specsys] observer = observer_location.transform_to(frame) if isinstance(frame, str): observer = attach_zero_velocities(observer) else: observer = update_differentials_to_match( observer_location, VELOCITY_FRAMES[self.wcs.specsys], preserve_observer_frame=True) elif self.wcs.specsys == 'TOPOCENT': observer = attach_zero_velocities(observer_location) else: raise NotImplementedError( f'SPECSYS={self.wcs.specsys} not yet supported') # Determine target # This is tricker. In principle the target for each pixel is the # celestial coordinates of the pixel, but we then need to be very # careful about SSYSOBS which is tricky. For now, we set the # target using the reference celestial coordinate in the WCS (if # any). if self.has_celestial and celestial_frame is not None: # NOTE: celestial_frame was defined higher up # NOTE: we set the distance explicitly to avoid warnings in SpectralCoord target = SkyCoord(self.wcs.crval[self.wcs.lng] * self.wcs.cunit[self.wcs.lng], self.wcs.crval[self.wcs.lat] * self.wcs.cunit[self.wcs.lat], frame=celestial_frame, distance=1000 * u.kpc) target = attach_zero_velocities(target) else: target = None # SpectralCoord does not work properly if either observer or target # are not convertible to ICRS, so if this is the case, we (for now) # drop the observer and target from the SpectralCoord and warn the # user. if observer is not None: try: observer.transform_to(ICRS()) except Exception: warnings.warn( 'observer cannot be converted to ICRS, so will ' 'not be set on SpectralCoord', AstropyUserWarning) observer = None if target is not None: try: target.transform_to(ICRS()) except Exception: warnings.warn( 'target cannot be converted to ICRS, so will ' 'not be set on SpectralCoord', AstropyUserWarning) target = None # NOTE: below we include Quantity in classes['spectral'] instead # of SpectralCoord - this is because we want to also be able to # accept plain quantities. if ctype == 'ZOPT': def spectralcoord_from_redshift(redshift): return SpectralCoord((redshift + 1) * self.wcs.restwav, unit=u.m, observer=observer, target=target) def redshift_from_spectralcoord(spectralcoord): # TODO: check target is consistent if observer is None: warnings.warn( 'No observer defined on WCS, SpectralCoord ' 'will be converted without any velocity ' 'frame change', AstropyUserWarning) return spectralcoord.to_value( u.m) / self.wcs.restwav - 1. else: return spectralcoord.in_observer_velocity_frame( observer).to_value(u.m) / self.wcs.restwav - 1. classes['spectral'] = (u.Quantity, (), {}, spectralcoord_from_redshift) components[self.wcs.spec] = ('spectral', 0, redshift_from_spectralcoord) elif ctype == 'BETA': def spectralcoord_from_beta(beta): return SpectralCoord(beta * C_SI, unit=u.m / u.s, doppler_convention='relativistic', doppler_rest=self.wcs.restwav * u.m, observer=observer, target=target) def beta_from_spectralcoord(spectralcoord): # TODO: check target is consistent doppler_equiv = u.doppler_relativistic(self.wcs.restwav * u.m) if observer is None: warnings.warn( 'No observer defined on WCS, SpectralCoord ' 'will be converted without any velocity ' 'frame change', AstropyUserWarning) return spectralcoord.to_value(u.m / u.s, doppler_equiv) / C_SI else: return spectralcoord.in_observer_velocity_frame( observer).to_value(u.m / u.s, doppler_equiv) / C_SI classes['spectral'] = (u.Quantity, (), {}, spectralcoord_from_beta) components[self.wcs.spec] = ('spectral', 0, beta_from_spectralcoord) else: kwargs['unit'] = self.wcs.cunit[ispec] if self.wcs.restfrq > 0: if ctype == 'VELO': kwargs['doppler_convention'] = 'relativistic' kwargs['doppler_rest'] = self.wcs.restfrq * u.Hz elif ctype == 'VRAD': kwargs['doppler_convention'] = 'radio' kwargs['doppler_rest'] = self.wcs.restfrq * u.Hz elif ctype == 'VOPT': kwargs['doppler_convention'] = 'optical' kwargs['doppler_rest'] = self.wcs.restwav * u.m def spectralcoord_from_value(value): return SpectralCoord(value, observer=observer, target=target, **kwargs) def value_from_spectralcoord(spectralcoord): # TODO: check target is consistent if observer is None: warnings.warn( 'No observer defined on WCS, SpectralCoord ' 'will be converted without any velocity ' 'frame change', AstropyUserWarning) return spectralcoord.to_value(**kwargs) else: return spectralcoord.in_observer_velocity_frame( observer).to_value(**kwargs) classes['spectral'] = (u.Quantity, (), {}, spectralcoord_from_value) components[self.wcs.spec] = ('spectral', 0, value_from_spectralcoord) # We can then make sure we correctly return Time objects where appropriate # (https://www.aanda.org/articles/aa/pdf/2015/02/aa24653-14.pdf) if 'time' in self.world_axis_physical_types: multiple_time = self.world_axis_physical_types.count('time') > 1 for i in range(self.naxis): if self.world_axis_physical_types[i] == 'time': if multiple_time: name = f'time.{i}' else: name = 'time' # Initialize delta reference_time_delta = None # Extract time scale scale = self.wcs.ctype[i].lower() if scale == 'time': if self.wcs.timesys: scale = self.wcs.timesys.lower() else: scale = 'utc' # Drop sub-scales if '(' in scale: pos = scale.index('(') scale, subscale = scale[:pos], scale[pos + 1:-1] warnings.warn( f'Dropping unsupported sub-scale ' f'{subscale.upper()} from scale {scale.upper()}', UserWarning) # TODO: consider having GPS as a scale in Time # For now GPS is not a scale, we approximate this by TAI - 19s if scale == 'gps': reference_time_delta = TimeDelta(19, format='sec') scale = 'tai' elif scale.upper() in FITS_DEPRECATED_SCALES: scale = FITS_DEPRECATED_SCALES[scale.upper()] elif scale not in Time.SCALES: raise ValueError( f'Unrecognized time CTYPE={self.wcs.ctype[i]}') # Determine location trefpos = self.wcs.trefpos.lower() if trefpos.startswith('topocent'): # Note that some headers use TOPOCENT instead of TOPOCENTER if np.any(np.isnan(self.wcs.obsgeo[:3])): warnings.warn( 'Missing or incomplete observer location ' 'information, setting location in Time to None', UserWarning) location = None else: location = EarthLocation(*self.wcs.obsgeo[:3], unit=u.m) elif trefpos == 'geocenter': location = EarthLocation(0, 0, 0, unit=u.m) elif trefpos == '': location = None else: # TODO: implement support for more locations when Time supports it warnings.warn( f"Observation location '{trefpos}' is not " "supported, setting location in Time to None", UserWarning) location = None reference_time = Time(np.nan_to_num(self.wcs.mjdref[0]), np.nan_to_num(self.wcs.mjdref[1]), format='mjd', scale=scale, location=location) if reference_time_delta is not None: reference_time = reference_time + reference_time_delta def time_from_reference_and_offset(offset): if isinstance(offset, Time): return offset return reference_time + TimeDelta(offset, format='sec') def offset_from_time_and_reference(time): return (time - reference_time).sec classes[name] = (Time, (), {}, time_from_reference_and_offset) components[i] = (name, 0, offset_from_time_and_reference) # Fallback: for any remaining components that haven't been identified, just # return Quantity as the class to use for i in range(self.naxis): if components[i] is None: name = self.wcs.ctype[i].split('-')[0].lower() if name == '': name = 'world' while name in classes: name += "_" classes[name] = (u.Quantity, (), {'unit': self.wcs.cunit[i]}) components[i] = (name, 0, 'value') # Keep a cached version of result self._components_and_classes_cache = wcs_hash, (components, classes) return components, classes
class Observer(): __names = [] def __init__(self, **kwargs): """ Defines the observer object Parameters: name (str): Name for the Observer. (required) Observer is uniquely defined (name must be different for each observer). code (str): The IAU code for SORA to search for its coordinates in MPC database site (EarthLocation): User provides an EarthLocation object. lon (str, float): The Longitude of the site in degrees. Positive to East. Range (0 to 360) or (-180 to +180) User can provide in degrees (float) or hexadecimal (string) lat (str, float): The Latitude of the site in degrees. Positive North. Range (+90 to -90) User can provide in degrees (float) or hexadecimal (string) height (int, float): The height of the site in meters above see level. Examples: User can provide one of the following to define an observer: - If user will use the MPC name for the site: Observer(code) - If user wants to use a different name from the MPC database: Observer(name, code) - If user wants to use an EarthLocation value: EarthLocation(lon, lat, height) Observer(name, site) - If user wants to give site coordinates directly: Observer(name, lon, lat, height) """ input_tests.check_kwargs( kwargs, allowed_kwargs=['code', 'height', 'lat', 'lon', 'name', 'site']) if 'code' in kwargs: self.code = kwargs['code'] try: name, self.site = search_code_mpc()[self.code] self.__name = kwargs.get('name', name) except: raise ValueError( 'code {} could not be located in MPC database'.format( self.code)) elif all(i in kwargs for i in ['name', 'site']): self.__name = kwargs['name'] self.site = test_attr(kwargs['site'], EarthLocation, 'site') elif all(i in kwargs for i in ['name', 'lon', 'lat', 'height']): self.__name = kwargs['name'] self.site = EarthLocation(kwargs['lon'], kwargs['lat'], kwargs['height']) else: raise ValueError('Input parameters could not be determined') if self.__name in self.__names: raise ValueError( "name {} already defined for another Observer object. " "Please choose a different one.".format(self.__name)) self.__names.append(self.__name) def get_ksi_eta(self, time, star): """ Calculates relative position to star in the orthographic projection. Parameters: time (str, Time): Reference time to calculate the position. It can be a string in the format "yyyy-mm-dd hh:mm:ss.s" or an astropy Time object star (str, SkyCoord): The coordinate of the star in the same reference frame as the ephemeris. It can be a string in the format "hh mm ss.s +dd mm ss.ss" or an astropy SkyCoord object. Returns: ksi, eta (float): on-sky orthographic projection of the observer relative to a star Ksi is in the North-South direction (North positive) Eta is in the East-West direction (East positive) """ time = test_attr(time, Time, 'time') try: star = SkyCoord(star, unit=(u.hourangle, u.deg)) except: raise ValueError( 'star is not an astropy object or a string in the format "hh mm ss.s +dd mm ss.ss"' ) itrs = self.site.get_itrs(obstime=time) gcrs = itrs.transform_to(GCRS(obstime=time)) rz = rotation_matrix(star.ra, 'z') ry = rotation_matrix(-star.dec, 'y') cp = gcrs.cartesian.transform(rz).transform(ry) return cp.y.to(u.km).value, cp.z.to(u.km).value def sidereal_time(self, time, mode='local'): """ Calculates the Apparent Sidereal Time at a reference time Parameters: time (str,Time): Reference time to calculate sidereal time. mode (str): local or greenwich If 'local': calculates the sidereal time for the coordinates of this object. If 'greenwich': calculates the Greenwich Apparent Sidereal Time. Returns: sidereal_time: An Astropy Longitude object with the Sidereal Time. """ # return local or greenwich sidereal time time = test_attr(time, Time, 'time') time.location = self.site if mode == 'local': return time.sidereal_time('apparent') elif mode == 'greenwich': return time.sidereal_time('apparent', 'greenwich') else: raise ValueError('mode must be "local" or "greenwich"') def altaz(self, time, coord): """ Calculates the Altitude and Azimuth at a reference time for a coordinate Parameters: time (str,Time): Reference time to calculate the sidereal time. coord (str, astropy.SkyCoord): Coordinate of the target ICRS. Returns: altitude (float): object altitude in degrees. azimuth (float): object azimuth in degrees. """ time = test_attr(time, Time, 'time') if type(coord) == str: coord = SkyCoord(coord, unit=(u.hourangle, u.deg)) ephem_altaz = coord.transform_to( AltAz(obstime=time, location=self.site)) return ephem_altaz.alt.deg, ephem_altaz.az.deg @property def name(self): return self.__name @property def lon(self): return self.site.lon @lon.setter def lon(self, lon): lat = self.site.lat height = self.site.height site = EarthLocation(lon, lat, height) self.site = site @property def lat(self): return self.site.lat @lat.setter def lat(self, lat): lon = self.site.lon height = self.site.height site = EarthLocation(lon, lat, height) self.site = site @property def height(self): return self.site.height @height.setter def height(self, height): lon = self.site.lon lat = self.site.lat site = EarthLocation(lon, lat, height) self.site = site def __str__(self): """ String representation of the Observer class """ out = ('Site: {}\n' 'Geodetic coordinates: Lon: {}, Lat: {}, height: {:.3f}'.format( self.name, self.site.lon.__str__(), self.site.lat.__str__(), self.site.height.to(u.km))) return out def __del__(self): """ When this object is deleted, it removes the name from the Class name list. """ try: self.__names.remove(self.__name) except: pass