def _pixel_scale_angle_at_skycoord(skycoord, wcs, offset=1.0 * u.arcsec): """ Calculate the pixel scale and WCS rotation angle at the position of a SkyCoord coordinate. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The SkyCoord coordinate. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS <https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_ (e.g. `astropy.wcs.WCS`, `gwcs.wcs.WCS`). offset : `~astropy.units.Quantity` A small angular offset to use to compute the pixel scale and position angle. Returns ------- scale : `~astropy.units.Quantity` The pixel scale in arcsec/pixel. angle : `~astropy.units.Quantity` The angle (in degrees) measured counterclockwise from the positive x axis to the "North" axis of the celestial coordinate system. Notes ----- If distortions are present in the image, the x and y pixel scales likely differ. This function computes a single pixel scale along the North/South axis. """ try: x, y = wcs.world_to_pixel(skycoord) # We take a point directly North (i.e. latitude offset) the input # sky coordinate and convert it to pixel coordinates, then we use the # pixel deltas between the input and offset sky coordinate to # calculate the pixel scale and angle. skycoord_offset = skycoord.directional_offset_by(0.0, offset) x_offset, y_offset = wcs.world_to_pixel(skycoord_offset) except AttributeError: # for Astropy < 3.1 WCS support x, y = skycoord_to_pixel(skycoord, wcs) coord = skycoord.represent_as('unitspherical') coord_new = UnitSphericalRepresentation(coord.lon, coord.lat + offset) coord_offset = skycoord.realize_frame(coord_new) x_offset, y_offset = skycoord_to_pixel(coord_offset, wcs) dx = x_offset - x dy = y_offset - y scale = offset.to(u.arcsec) / (np.hypot(dx, dy) * u.pixel) angle = (np.arctan2(dy, dx) * u.radian).to(u.deg) return scale, angle
def test_camera_tilt(self): """Apply, then remove camera tilt and check that result is same as starting coordinate""" param_set = ModelParamSet(None, None, None, None) model = MountModel(param_set) for _ in range(20): params = ModelParameters( axis_0_offset=Angle( 0 * u.deg), # does not affect the method under test axis_1_offset=Angle( 0 * u.deg), # does not affect the method under test pole_rot_axis_az=Angle( 0 * u.deg), # does not affect the method under test pole_rot_angle=Angle( 0 * u.deg), # does not affect the method under test camera_tilt=Angle(np.random.uniform(-90.0, 90.0) * u.deg), ) model.model_params = params coord = UnitSphericalRepresentation( np.random.uniform(0.0, 360.0) * u.deg, np.random.uniform(-90.0, 90.0) * u.deg) meridian_side = random.choice( (MeridianSide.WEST, MeridianSide.EAST)) tilted_coord = model.apply_camera_tilt(coord, meridian_side) final_coord = model.remove_camera_tilt(tilted_coord, meridian_side) assertSphericalClose(coord, final_coord)
def nominal_to_altaz(norm_coord, altaz_coord): """ Transformation from nominal system to astropy AltAz system Parameters ---------- norm_coord: `astropy.coordinates.SkyCoord` nominal system altaz_coord: `astropy.coordinates.SkyCoord` AltAz system Returns ------- AltAz Coordinates """ alt_norm, az_norm = norm_coord.array_direction.alt, norm_coord.array_direction.az if type(norm_coord.x.value).__module__ != np.__name__: x_off = np.zeros(1) x_off[0] = norm_coord.x.value x_off = x_off * norm_coord.x.unit y_off = np.zeros(1) y_off[0] = norm_coord.y.value y_off = y_off * norm_coord.y.unit else: x_off = norm_coord.x y_off = norm_coord.y altitude, azimuth = offset_to_altaz(x_off, y_off, az_norm, alt_norm) representation = UnitSphericalRepresentation(lon=azimuth.to(u.deg), lat=altitude.to(u.deg)) return altaz_coord.realize_frame(representation)
def test_camera_tilt_inverse(self): """Remove, then apply camera tilt for reachable positions and check result""" param_set = ModelParamSet(None, None, None, None) model = MountModel(param_set) for _ in range(20): tilt = Angle(np.random.uniform(-90.0, 90.0) * u.deg) params = ModelParameters( axis_0_offset=Angle( 0 * u.deg), # does not affect the method under test axis_1_offset=Angle( 0 * u.deg), # does not affect the method under test pole_rot_axis_az=Angle( 0 * u.deg), # does not affect the method under test pole_rot_angle=Angle( 0 * u.deg), # does not affect the method under test camera_tilt=tilt, ) model.model_params = params coord = UnitSphericalRepresentation( np.random.uniform(0.0, 360.0) * u.deg, np.random.uniform(-90.0 + abs(tilt.deg), 90.0 - abs(tilt.deg)) * u.deg) meridian_side = random.choice( (MeridianSide.WEST, MeridianSide.EAST)) untilted_coord = model.remove_camera_tilt(coord, meridian_side) final_coord = model.apply_camera_tilt(untilted_coord, meridian_side) assertSphericalClose(coord, final_coord)
def gcrs_to_geosolarecliptic(gcrs_coo, to_frame): if not to_frame.obstime.isscalar: raise ValueError( "To perform this transformation the obstime Attribute must be a scalar." ) _earth_orbit_perpen_point_gcrs = UnitSphericalRepresentation( lon=0 * u.deg, lat=(90 * u.deg - _obliquity_rotation_value(to_frame.obstime))) _earth_detilt_matrix = _make_rotation_matrix_from_reprs( _earth_orbit_perpen_point_gcrs, CartesianRepresentation(0, 0, 1)) sun_pos_gcrs = get_body("sun", to_frame.obstime).cartesian earth_pos_gcrs = get_body("earth", to_frame.obstime).cartesian sun_earth = sun_pos_gcrs - earth_pos_gcrs sun_earth_detilt = sun_earth.transform(_earth_detilt_matrix) # Earth-Sun Line in Geocentric Solar Ecliptic Frame x_axis = CartesianRepresentation(1, 0, 0) rot_matrix = _make_rotation_matrix_from_reprs(sun_earth_detilt, x_axis) return matrix_product(rot_matrix, _earth_detilt_matrix)
def camera_to_telescope(camera_coord, telescope_frame): ''' Transformation between CameraFrame and TelescopeFrame. Is called when a SkyCoord is transformed from CameraFrame into TelescopeFrame ''' x_pos = camera_coord.cartesian.x y_pos = camera_coord.cartesian.y rot = camera_coord.rotation if rot == 0: # if no rotation applied save a few cycles x_rotated = x_pos y_rotated = y_pos else: cosrot = np.cos(rot) sinrot = np.sin(rot) x_rotated = x_pos * cosrot - y_pos * sinrot y_rotated = x_pos * sinrot + y_pos * cosrot focal_length = camera_coord.focal_length # this assumes an equidistant mapping function of the telescope optics # or a small angle approximation of most other mapping functions # this could be replaced by actually defining the mapping function # as an Attribute of `CameraFrame` that maps f(r, focal_length) -> theta, # where theta is the angle to the optical axis and r is the distance # to the camera center in the focal plane delta_alt = u.Quantity((x_rotated / focal_length).to_value(u.dimensionless_unscaled), u.rad) delta_az = u.Quantity((y_rotated / focal_length).to_value(u.dimensionless_unscaled), u.rad) representation = UnitSphericalRepresentation(lat=delta_alt, lon=delta_az) return telescope_frame.realize_frame(representation)
def setup_grid(self, wavelength=656*u.nm): # use HEALPIX to get evenly sized tiles NSIDE = hp.npix2nside(self.ntiles) colat, lon = hp.pix2ang(NSIDE, np.arange(0, self.ntiles)) # co-latitude theta_values = u.Quantity(colat, unit=u.rad) # longitude phi_values = u.Quantity(lon, unit=u.rad) # the following formulae use the Roche approximation and assume # solid body rotation # solve for radius of rotating star at these co-latitudes if self.distortion: radii = self.radius*np.array([newton(surface, 1.01, args=(self.omega, x)) for x in theta_values]) else: radii = self.radius*np.ones(self.ntiles) # and effective gravities geff = np.sqrt((-const.G*self.mass/radii**2 + self.Omega**2 * radii * np.sin(theta_values)**2)**2 + self.Omega**4 * radii**2 * np.sin(theta_values)**2 * np.cos(theta_values)**2) # now make a ntiles sized CartesianRepresentation of positions self.tile_locs = SphericalRepresentation(phi_values, 90*u.deg-theta_values, radii).to_cartesian() # normal to tile is the direction of the derivate of the potential # this is the vector form of geff above # the easiest way to express it is that it differs from (r, theta, phi) # by a small amount in the theta direction epsilon x = radii/self.radius a = 1./x**2 - (8./27.)*self.omega**2 * x * np.sin(theta_values)**2 b = np.sqrt( (-1./x**2 + (8./27)*self.omega**2 * x * np.sin(theta_values)**2)**2 + ((8./27)*self.omega**2 * x * np.sin(theta_values) * np.cos(theta_values))**2 ) epsilon = np.arccos(a/b) self.tile_dirs = UnitSphericalRepresentation(phi_values, 90*u.deg - theta_values - epsilon) self.tile_dirs = self.tile_dirs.to_cartesian() # and ntiles sized arrays of tile properties tile_temperatures = 2000.0 * u.K * (geff / geff.max())**self.beta # fluxes, not accounting for limb darkening self.tile_scales = np.ones(self.ntiles) self.tile_fluxes = blackbody_nu(wavelength, tile_temperatures) # tile areas spher = self.tile_locs.represent_as(SphericalRepresentation) self.tile_areas = spher.distance**2 * hp.nside2pixarea(NSIDE) * u.rad * u.rad omega_vec = CartesianRepresentation( u.Quantity([0.0, 0.0, self.Omega.value], unit=self.Omega.unit) ) # get velocities of tiles self.tile_velocities = cross(omega_vec, self.tile_locs)
def skycoord_to_pixel_scale_angle(skycoord, wcs, small_offset=1 * u.arcsec): """ Convert a set of SkyCoord coordinates into pixel coordinates, pixel scales, and position angles. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` Sky coordinates wcs : `~astropy.wcs.WCS` The WCS transformation to use small_offset : `~astropy.units.Quantity` A small offset to use to compute the angle Returns ------- pixcoord : `~regions.PixCoord` Pixel coordinates scale : float The pixel scale at each location, in degrees/pixel angle : `~astropy.units.Quantity` The position angle of the celestial coordinate system in pixel space. """ # Convert to pixel coordinates x, y = skycoord_to_pixel(skycoord, wcs, mode=skycoord_to_pixel_mode) pixcoord = PixCoord(x=x, y=y) # We take a point directly 'above' (in latitude) the position requested # and convert it to pixel coordinates, then we use that to figure out the # scale and position angle of the coordinate system at the location of # the points. # Find the coordinates as a representation object r_old = skycoord.represent_as('unitspherical') # Add a a small perturbation in the latitude direction (since longitude # is more difficult because it is not directly an angle). dlat = small_offset r_new = UnitSphericalRepresentation(r_old.lon, r_old.lat + dlat) coords_offset = skycoord.realize_frame(r_new) # Find pixel coordinates of offset coordinates x_offset, y_offset = skycoord_to_pixel(coords_offset, wcs, mode=skycoord_to_pixel_mode) # Find vector dx = x_offset - x dy = y_offset - y # Find the length of the vector scale = np.hypot(dx, dy) / dlat.to('degree').value # Find the position angle angle = np.arctan2(dy, dx) * u.radian return pixcoord, scale, angle
def test_regression_6236(): # sunpy changes its representation upon initialisation of a frame, # including via `realize_frame`. Ensure this works. class MyFrame(BaseCoordinateFrame): default_representation = CartesianRepresentation my_attr = QuantityAttribute(default=0, unit=u.m) class MySpecialFrame(MyFrame): def __init__(self, *args, **kwargs): _rep_kwarg = kwargs.get('representation_type', None) super().__init__(*args, **kwargs) if not _rep_kwarg: self.representation_type = self.default_representation self._data = self.data.represent_as(self.representation_type) rep1 = UnitSphericalRepresentation([0., 1] * u.deg, [2., 3.] * u.deg) rep2 = SphericalRepresentation([10., 11] * u.deg, [12., 13.] * u.deg, [14., 15.] * u.kpc) mf1 = MyFrame(rep1, my_attr=1. * u.km) mf2 = mf1.realize_frame(rep2) # Normally, data is stored as is, but the representation gets set to a # default, even if a different representation instance was passed in. # realize_frame should do the same. Just in case, check attrs are passed. assert mf1.data is rep1 assert mf2.data is rep2 assert mf1.representation_type is CartesianRepresentation assert mf2.representation_type is CartesianRepresentation assert mf2.my_attr == mf1.my_attr # It should be independent of whether I set the reprensentation explicitly mf3 = MyFrame(rep1, my_attr=1. * u.km, representation_type='unitspherical') mf4 = mf3.realize_frame(rep2) assert mf3.data is rep1 assert mf4.data is rep2 assert mf3.representation_type is UnitSphericalRepresentation assert mf4.representation_type is CartesianRepresentation assert mf4.my_attr == mf3.my_attr # This should be enough to help sunpy, but just to be sure, a test # even closer to what is done there, i.e., transform the representation. msf1 = MySpecialFrame(rep1, my_attr=1. * u.km) msf2 = msf1.realize_frame(rep2) assert msf1.data is not rep1 # Gets transformed to Cartesian. assert msf2.data is not rep2 assert type(msf1.data) is CartesianRepresentation assert type(msf2.data) is CartesianRepresentation assert msf1.representation_type is CartesianRepresentation assert msf2.representation_type is CartesianRepresentation assert msf2.my_attr == msf1.my_attr # And finally a test where the input is not transformed. msf3 = MySpecialFrame(rep1, my_attr=1. * u.km, representation_type='unitspherical') msf4 = msf3.realize_frame(rep2) assert msf3.data is rep1 assert msf4.data is not rep2 assert msf3.representation_type is UnitSphericalRepresentation assert msf4.representation_type is CartesianRepresentation assert msf4.my_attr == msf3.my_attr
def test_cirs_altaz_nodist(testframe): """ Check that a UnitSphericalRepresentation coordinate round-trips for the CIRS<->AltAz transformation. """ coo0 = CIRS(UnitSphericalRepresentation(10*u.deg, 20*u.deg), obstime=testframe.obstime) # check that it round-trips coo1 = coo0.transform_to(testframe).transform_to(coo0) assert_allclose(coo0.cartesian.xyz, coo1.cartesian.xyz)
def skycoord_to_pixel_scale_angle(coords, wcs): """ Convert a set of SkyCoord coordinates into pixel coordinates, pixel scales, and position angles. Parameters ---------- coords : `~astropy.coordinates.SkyCoord` The coordinates to convert wcs : `~astropy.wcs.WCS` The WCS transformation to use Returns ------- x, y : `~numpy.ndarray` The x and y pixel coordinates corresponding to the input coordinates scale : `~astropy.units.Quantity` The pixel scale at each location, in degrees/pixel angle : `~astropy.units.Quantity` The position angle of the celestial coordinate system in pixel space. """ # Convert to pixel coordinates x, y = skycoord_to_pixel(coords, wcs, mode=skycoord_to_pixel_mode) # We take a point directly 'above' (in latitude) the position requested # and convert it to pixel coordinates, then we use that to figure out the # scale and position angle of the coordinate system at the location of # the points. # Find the coordinates as a representation object r_old = coords.represent_as('unitspherical') # Add a a small perturbation in the latitude direction (since longitude # is more difficult because it is not directly an angle). dlat = 1 * u.arcsec r_new = UnitSphericalRepresentation(r_old.lon, r_old.lat + dlat) coords_offset = coords.realize_frame(r_new) # Find pixel coordinates of offset coordinates x_offset, y_offset = skycoord_to_pixel(coords_offset, wcs, mode=skycoord_to_pixel_mode) # Find vector dx = x_offset - x dy = y_offset - y # Find the length of the vector scale = np.hypot(dx, dy) * u.pixel / dlat # Find the position angle angle = np.arctan2(dy, dx) * u.radian return x, y, scale, angle
def _convert_wcs(lon_in, lat_in, frame_in, frame_out): """Convert (longitude, latitude) coordinates from the input frame to the specified output frame. Parameters ---------- lon_in : 1D `~numpy.ndarray` The longitude to convert, unit degree, [0, 360) lat_in : 1D `~numpy.ndarray` The latitude to convert, unit degree, [-90, 90] frame_in, frame_out : tuple or `~astropy.wcs.WCS` The input and output frames, which can be passed either as a tuple of ``(frame, lon_unit, lat_unit)`` or as a `~astropy.wcs.WCS` instance. Returns ------- lon_out, lat_out : 1D `~numpy.ndarray` Output longitude and latitude in the output frame References ---------- [1] reproject - wcs_utils.convert_world_coordinates() https://github.com/astrofrog/reproject """ if isinstance(frame_in, WCS): coordframe_in = wcs_to_celestial_frame(frame_in) lon_in_unit = au.Unit(frame_in.wcs.cunit[0]) lat_in_unit = au.Unit(frame_in.wcs.cunit[1]) else: coordframe_in, lon_in_unit, lat_in_unit = frame_in # if isinstance(frame_out, WCS): coordframe_out = wcs_to_celestial_frame(frame_out) lon_out_unit = au.Unit(frame_out.wcs.cunit[0]) lat_out_unit = au.Unit(frame_out.wcs.cunit[1]) else: coordframe_out, lon_out_unit, lat_out_unit = frame_out # logger.info("Convert coordinates from {0} to {1}".format( coordframe_in, coordframe_out)) logger.info("Input coordinates units: " "{0} (longitude), {1} (latitude)".format( lon_in_unit, lat_in_unit)) logger.info("Output coordinates units: " "{0} (longitude), {1} (latitude)".format( lon_out_unit, lat_out_unit)) # data = UnitSphericalRepresentation(lon_in * lon_in_unit, lat_in * lat_in_unit) coords_in = coordframe_in.realize_frame(data) coords_out = coords_in.transform_to(coordframe_out) data_out = coords_out.represent_as("unitspherical") lon_out = data_out.lon.to(lon_out_unit).value lat_out = data_out.lat.to(lon_out_unit).value return lon_out, lat_out
def pixel_scale_angle_at_skycoord(skycoord, wcs, offset=1. * u.arcsec): """ Calculate the pixel scale and WCS rotation angle at the position of a SkyCoord coordinate. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The SkyCoord coordinate. wcs : `~astropy.wcs.WCS` The world coordinate system (WCS) transformation to use. offset : `~astropy.units.Quantity` A small angular offset to use to compute the pixel scale and position angle. Returns ------- scale : `~astropy.units.Quantity` The pixel scale in arcsec/pixel. angle : `~astropy.units.Quantity` The angle (in degrees) measured counterclockwise from the positive x axis to the "North" axis of the celestial coordinate system. Notes ----- If distortions are present in the image, the x and y pixel scales likely differ. This function computes a single pixel scale along the North/South axis. """ # We take a point directly "above" (in latitude) the input position # and convert it to pixel coordinates, then we use the pixel deltas # between the input and offset point to calculate the pixel scale and # angle. # Find the coordinates as a representation object coord = skycoord.represent_as('unitspherical') # Add a a small perturbation in the latitude direction (since longitude # is more difficult because it is not directly an angle) coord_new = UnitSphericalRepresentation(coord.lon, coord.lat + offset) coord_offset = skycoord.realize_frame(coord_new) # Find pixel coordinates of offset coordinates and pixel deltas x_offset, y_offset = skycoord_to_pixel(coord_offset, wcs, mode='all') x, y = skycoord_to_pixel(skycoord, wcs, mode='all') dx = x_offset - x dy = y_offset - y scale = offset.to(u.arcsec) / (np.hypot(dx, dy) * u.pixel) angle = (np.arctan2(dy, dx) * u.radian).to(u.deg) return scale, angle
def convert_world_coordinates(lon_in, lat_in, wcs_in, wcs_out): """ Convert longitude/latitude coordinates from an input frame to an output frame. Parameters ---------- lon_in, lat_in : `~numpy.ndarray` The longitude and latitude to convert wcs_in, wcs_out : tuple or `~astropy.wcs.WCS` The input and output frames, which can be passed either as a tuple of ``(frame, lon_unit, lat_unit)`` or as a `~astropy.wcs.WCS` instance. Returns ------- lon_out, lat_out : `~numpy.ndarray` The output longitude and latitude """ if isinstance(wcs_in, WCS): # Extract the celestial component of the WCS in (lon, lat) order wcs_in = wcs_in.celestial frame_in = wcs_to_celestial_frame(wcs_in) lon_in_unit = u.Unit(wcs_in.wcs.cunit[0]) lat_in_unit = u.Unit(wcs_in.wcs.cunit[1]) else: frame_in, lon_in_unit, lat_in_unit = wcs_in if isinstance(wcs_out, WCS): # Extract the celestial component of the WCS in (lon, lat) order wcs_out = wcs_out.celestial frame_out = wcs_to_celestial_frame(wcs_out) lon_out_unit = u.Unit(wcs_out.wcs.cunit[0]) lat_out_unit = u.Unit(wcs_out.wcs.cunit[1]) else: frame_out, lon_out_unit, lat_out_unit = wcs_out data = UnitSphericalRepresentation(lon_in * lon_in_unit, lat_in * lat_in_unit) coords_in = frame_in.realize_frame(data) coords_out = coords_in.transform_to(frame_out) lon_out = coords_out.represent_as('unitspherical').lon.to( lon_out_unit).value lat_out = coords_out.represent_as('unitspherical').lat.to( lat_out_unit).value return lon_out, lat_out
def setup(self): self.coord_scalar = SkyCoord(1, 2, unit='deg', frame='icrs') lon, lat = np.ones((2, 1000)) self.coord_array_1e3 = SkyCoord(lon, lat, unit='deg', frame='icrs') self.lon_1e6, self.lat_1e6 = np.ones((2, int(1e6))) self.coord_array_1e6 = SkyCoord(self.lon_1e6, self.lat_1e6, unit='deg', frame='icrs') self.scalar_q_ra = 1 * u.deg self.scalar_q_dec = 2 * u.deg np.random.seed(12345) self.array_q_ra = np.random.rand(int(1e6)) * 360 * u.deg self.array_q_dec = (np.random.rand(int(1e6)) * 180 - 90) * u.deg self.scalar_repr = UnitSphericalRepresentation(lat=self.scalar_q_dec, lon=self.scalar_q_ra) self.array_repr = UnitSphericalRepresentation(lat=self.array_q_dec, lon=self.array_q_ra)
def latlon_to_cartesian(latitude, longitude, stellar_inclination): """ Convert coordinates in latitude/longitude for a star with a given stellar inclination into cartesian coordinates. The X-Y plane is the sky plane: x is aligned with the stellar equator, y is aligned with the stellar rotation axis. Parameters ---------- latitude : float or `~astropy.units.Quantity` Spot latitude. Will assume unit=deg if none is specified. longitude : float or `~astropy.units.Quantity` Spot longitude. Will assume unit=deg if none is specified. stellar_inclination : float Stellar inclination angle, measured away from the line of sight, in [deg]. Returns ------- cartesian : `~astropy.coordinates.CartesianRepresentation` Cartesian representation in the frame described above. """ if not hasattr(longitude, 'unit') and not hasattr(latitude, 'unit'): longitude *= u.deg latitude *= u.deg c = UnitSphericalRepresentation(longitude, latitude) cartesian = c.to_cartesian() rotate_about_z = rotation_matrix(90 * u.deg, axis='z') rotate_is = rotation_matrix(stellar_inclination * u.deg, axis='y') transform_matrix = matrix_product(rotate_about_z, rotate_is) cartesian = cartesian.transform(transform_matrix) return cartesian
def _convert_world_coordinates(lon_in, lat_in, wcs_in, wcs_out): frame_in, lon_in_unit, lat_in_unit = wcs_in wcs_out = wcs_out.celestial frame_out = wcs_to_celestial_frame(wcs_out) lon_out_unit = u.Unit(wcs_out.wcs.cunit[0]) lat_out_unit = u.Unit(wcs_out.wcs.cunit[1]) data = UnitSphericalRepresentation(lon_in * lon_in_unit, lat_in * lat_in_unit) coords_in = frame_in.realize_frame(data) coords_out = coords_in.transform_to(frame_out) lon_out = coords_out.represent_as('unitspherical').lon.to( lon_out_unit).value lat_out = coords_out.represent_as('unitspherical').lat.to( lat_out_unit).value return lon_out, lat_out
def transform(self, input_coords): """ Transform one set of coordinates to another """ if self.same_frames: return input_coords input_coords = input_coords * u.deg x_in, y_in = input_coords[:, 0], input_coords[:, 1] c_in = SkyCoord(UnitSphericalRepresentation(x_in, y_in), frame=self.input_system) # We often need to transform arrays that contain NaN values, and filtering # out the NaN values would have a performance hit, so instead we just pass # on all values and just ignore Numpy warnings with np.errstate(all='ignore'): c_out = c_in.transform_to(self.output_system) lon = c_out.spherical.lon.deg lat = c_out.spherical.lat.deg return np.concatenate((lon[:, np.newaxis], lat[:, np.newaxis]), axis=1)
def test_camera_tilt_unreachable(self): """Remove, then apply camera tilt for unreachable positions and check separation""" param_set = ModelParamSet(None, None, None, None) model = MountModel(param_set) for _ in range(20): tilt = Angle(np.random.uniform(-90.0, 90.0) * u.deg) params = ModelParameters( axis_0_offset=Angle( 0 * u.deg), # does not affect the method under test axis_1_offset=Angle( 0 * u.deg), # does not affect the method under test pole_rot_axis_az=Angle( 0 * u.deg), # does not affect the method under test pole_rot_angle=Angle( 0 * u.deg), # does not affect the method under test camera_tilt=tilt, ) model.model_params = params coord = UnitSphericalRepresentation( np.random.uniform(0.0, 360.0) * u.deg, random.choice((-1, +1)) * np.random.uniform(90.0 - abs(tilt.deg), 90.0) * u.deg) meridian_side = random.choice( (MeridianSide.WEST, MeridianSide.EAST)) untilted_coord = model.remove_camera_tilt(coord, meridian_side) final_coord = model.apply_camera_tilt(untilted_coord, meridian_side) separation = SkyCoord(coord).separation(SkyCoord(final_coord)) expected_separation = np.abs(tilt) - (90 * u.deg - np.abs(coord.lat)) assert_allclose(separation.deg, expected_separation.deg)
def convert_world_coordinates(xw_in, yw_in, wcs_in, wcs_out): """ Convert world coordinates from an input frame to an output frame. Parameters ---------- xw_in, yw_in : `~numpy.ndarray` The input coordinates to convert wcs_in, wcs_out : tuple or `~astropy.wcs.WCS` The input and output frames, which can be passed either as a tuple of ``(frame, x_unit, y_unit)`` or as a `~astropy.wcs.WCS` instance. """ if isinstance(wcs_in, WCS): frame_in = wcs_to_celestial_frame(wcs_in) xw_in_unit = u.Unit(wcs_in.wcs.cunit[0]) yw_in_unit = u.Unit(wcs_in.wcs.cunit[1]) else: frame_in, xw_in_unit, yw_in_unit = wcs_in if isinstance(wcs_out, WCS): frame_out = wcs_to_celestial_frame(wcs_out) xw_out_unit = u.Unit(wcs_out.wcs.cunit[0]) yw_out_unit = u.Unit(wcs_out.wcs.cunit[1]) else: frame_out, xw_out_unit, yw_out_unit = wcs_out data = UnitSphericalRepresentation(xw_in * xw_in_unit, yw_in * yw_in_unit) coords_in = frame_in.realize_frame(data) coords_out = coords_in.transform_to(frame_out) xw_out = coords_out.spherical.lon.to(xw_out_unit).value yw_out = coords_out.spherical.lat.to(yw_out_unit).value return xw_out, yw_out
def surface_normal(self, lat, lon): usr = UnitSphericalRepresentation(lon=lon+self.rotation_angle, lat=lat) rotm = matrix_utilities.rotation_matrix(self.obliquity, axis='x') return usr.to_cartesian().transform(rotm)
def test_skycoord(frame): c = SkyCoord([[1, 2], [3, 4]], [[5, 6], [7, 8]], unit='deg', frame=frame, obstime=Time('2016-01-02'), location=EarthLocation(1000, 2000, 3000, unit=u.km)) cy = load(dump(c)) compare_coord(c, cy) @pytest.mark.parametrize('rep', [ CartesianRepresentation(1*u.m, 2.*u.m, 3.*u.m), SphericalRepresentation([[1, 2], [3, 4]]*u.deg, [[5, 6], [7, 8]]*u.deg, 10*u.pc), UnitSphericalRepresentation(0*u.deg, 10*u.deg), SphericalCosLatDifferential([[1.], [2.]]*u.mas/u.yr, [4., 5.]*u.mas/u.yr, [[[10]], [[20]]]*u.km/u.s), CartesianDifferential([10, 20, 30]*u.km/u.s), CartesianRepresentation( [1, 2, 3]*u.m, differentials=CartesianDifferential([10, 20, 30]*u.km/u.s)), SphericalRepresentation( [[1, 2], [3, 4]]*u.deg, [[5, 6], [7, 8]]*u.deg, 10*u.pc, differentials={ 's': SphericalDifferential([[0., 1.], [2., 3.]]*u.mas/u.yr, [[4., 5.], [6., 7.]]*u.mas/u.yr, 10*u.km/u.s)})]) def test_representations(rep): rrep = load(dump(rep))
def spherical_to_cartesian(self, spot_lons, spot_lats, inc_stellar, times=None, planet=None, time_ref=None): """ Convert spot parameter matrices in the original stellar coordinates to rotated and tilted cartesian coordinates. Parameters ---------- spot_lons : `~astropy.units.Quantity` Spot longitudes spot_lats : `~astropy.units.Quantity` Spot latitudes inc_stellar : `~astropy.units.Quantity` Stellar inclination times : `~numpy.ndarray` Times at which evaluate the stellar rotation planet : `~batman.TransitParams` Planet parameters time_ref : float Reference time used as the initial rotational phase of the star, such that the sub-observer point is at zero longitude at ``time_ref``. Returns ------- tilted_spots : `~numpy.ndarray` Rotated and tilted spot positions in cartesian coordinates """ # Spots by default are given in unit spherical representation (lat, lon) usr = UnitSphericalRepresentation(spot_lons, spot_lats) # Represent those spots with cartesian coordinates (x, y, z) # In this coordinate system, the observer is at positive x->inf, # the star is at the origin, and (y, z) is the sky plane. cartesian = usr.represent_as(CartesianRepresentation) # Generate array of rotation matrices to rotate the spots about the # stellar rotation axis if times is None or (hasattr(times, '__len__') and times[0] is None): rotate = rotation_matrix(self.phases[:, np.newaxis, np.newaxis], axis='z') else: if time_ref is None: time_ref = 0 rotational_phase = 2 * np.pi * ( (times - time_ref) / self.rotation_period) * u.rad rotate = rotation_matrix(rotational_phase[:, np.newaxis, np.newaxis], axis='z') rotated_spots = cartesian.transform(rotate) if planet is not None and hasattr(planet, 'lam'): lam = planet.lam * u.deg else: lam = 0 * u.deg # Generate array of rotation matrices to rotate the spots so that the # star is observed from the correct stellar inclination stellar_inclination = rotation_matrix(inc_stellar - 90 * u.deg, axis='y') inclined_spots = rotated_spots.transform(stellar_inclination) # Generate array of rotation matrices to rotate the spots so that the # planet's orbit normal is tilted with respect to stellar spin tilt = rotation_matrix(-lam, axis='x') tilted_spots = inclined_spots.transform(tilt) return tilted_spots
class Star: mass = AffectsOmegaCrit('mass', u.kg) radius = AffectsOmegaCrit('radius', u.m) beta = ResetsGrid('beta') distortion = ResetsGrid('distortion') @u.quantity_input(mass=u.kg) @u.quantity_input(radius=u.m) @u.quantity_input(period=u.s) def __init__(self, mass, radius, period, beta=0.08, ulimb=0.9, ntiles=3072, distortion=True): self.distortion = distortion self.mass = mass self.radius = radius # will also set self.omega_crit self.beta = beta self.ulimb = ulimb self.ntiles = ntiles self.period = period self.clear_grid() def clear_grid(self): # now set up tile locations, velocities and directions. # These will be (nlon, nlat) CartesianRepresentations self.tile_locs = None self.tile_dirs = None self.tile_velocities = None # and arrays of tile properties - shape (nlon, nlat) self.tile_areas = None self.tile_fluxes = None # the next array is the main one that gets tweaked self.tile_scales = None """ We define many of the attributes as properties, so we can wipe the grid when they are set, and also check for violation of critical rotation """ @property def omega(self): """ Ratio of angular velocity to critical number. read-only property """ return (self.Omega/self.omega_crit).decompose() @property def period(self): return 2.0*np.pi/self.Omega @period.setter @u.quantity_input(value=u.s) def period(self, value): Omega = 2.0*np.pi/value if (Omega/self.omega_crit).decompose() > 1: raise ValueError('This rotation period exceeds critical value') self.Omega = Omega self.clear_grid() """ ntiles is also a property, since we need to remap to an appropriate value for HEALPIX. """ @property def ntiles(self): """ Number of tiles. Is checked to see if appropriate for HEALPIX algorithm. """ return self._ntiles @ntiles.setter def ntiles(self, value): allowed_values = [48, 192, 768, 3072, 12288, 49152, 196608] if int(value) not in allowed_values: raise ValueError('{} not one of allowed values: {!r}'.format( value, allowed_values )) self._ntiles = int(12*np.floor(np.sqrt(value/12.))**2) self.clear_grid() @u.quantity_input(wavelength=u.nm) def setup_grid(self, wavelength=656*u.nm): # use HEALPIX to get evenly sized tiles NSIDE = hp.npix2nside(self.ntiles) colat, lon = hp.pix2ang(NSIDE, np.arange(0, self.ntiles)) # co-latitude theta_values = u.Quantity(colat, unit=u.rad) # longitude phi_values = u.Quantity(lon, unit=u.rad) # the following formulae use the Roche approximation and assume # solid body rotation # solve for radius of rotating star at these co-latitudes if self.distortion: radii = self.radius*np.array([newton(surface, 1.01, args=(self.omega, x)) for x in theta_values]) else: radii = self.radius*np.ones(self.ntiles) # and effective gravities geff = np.sqrt((-const.G*self.mass/radii**2 + self.Omega**2 * radii * np.sin(theta_values)**2)**2 + self.Omega**4 * radii**2 * np.sin(theta_values)**2 * np.cos(theta_values)**2) # now make a ntiles sized CartesianRepresentation of positions self.tile_locs = SphericalRepresentation(phi_values, 90*u.deg-theta_values, radii).to_cartesian() # normal to tile is the direction of the derivate of the potential # this is the vector form of geff above # the easiest way to express it is that it differs from (r, theta, phi) # by a small amount in the theta direction epsilon x = radii/self.radius a = 1./x**2 - (8./27.)*self.omega**2 * x * np.sin(theta_values)**2 b = np.sqrt( (-1./x**2 + (8./27)*self.omega**2 * x * np.sin(theta_values)**2)**2 + ((8./27)*self.omega**2 * x * np.sin(theta_values) * np.cos(theta_values))**2 ) epsilon = np.arccos(a/b) self.tile_dirs = UnitSphericalRepresentation(phi_values, 90*u.deg - theta_values - epsilon) self.tile_dirs = self.tile_dirs.to_cartesian() # and ntiles sized arrays of tile properties tile_temperatures = 2000.0 * u.K * (geff / geff.max())**self.beta # fluxes, not accounting for limb darkening self.tile_scales = np.ones(self.ntiles) self.tile_fluxes = blackbody_nu(wavelength, tile_temperatures) # tile areas spher = self.tile_locs.represent_as(SphericalRepresentation) self.tile_areas = spher.distance**2 * hp.nside2pixarea(NSIDE) * u.rad * u.rad omega_vec = CartesianRepresentation( u.Quantity([0.0, 0.0, self.Omega.value], unit=self.Omega.unit) ) # get velocities of tiles self.tile_velocities = cross(omega_vec, self.tile_locs) @u.quantity_input(inclination=u.deg) def plot(self, inclination=90*u.deg, phase=0.0, savefig=False, filename='star_surface.png', cmap='magma', what='fluxes', cstride=1, rstride=1, shade=False): ax = plt.axes(projection='3d') ax.view_init(90-inclination.to(u.deg).value, 360*phase) # get map values if what == 'fluxes': vals = self.tile_fluxes * self.tile_scales vals = vals / vals.max() elif what == 'vels': earth = set_earth(inclination.to(u.deg).value, phase) velocities = self.tile_velocities.xyz vals = dot(earth, velocities).to(u.km/u.s) # can't plot negative values, so rescale from 0 - 1 vals = (vals - vals.min())/(vals.max()-vals.min()) elif what == 'areas': vals = self.tile_areas / self.tile_areas.max() colors = getattr(cm, cmap)(vals.value) # project the map to a rectangular matrix nlat = nlon = int(np.floor(np.sqrt(self.ntiles))) theta = np.linspace(np.pi, 0, nlat) phi = np.linspace(-np.pi, np.pi, nlon) PHI, THETA = np.meshgrid(phi, theta) NSIDE = hp.npix2nside(self.ntiles) grid_pix = hp.ang2pix(NSIDE, THETA, PHI) grid_map = colors[grid_pix] # Create a sphere r = 0.3 x = r*np.sin(THETA)*np.cos(PHI) y = r*np.sin(THETA)*np.sin(PHI) z = r*np.cos(THETA) ax.plot_surface(x, y, z, cstride=cstride, rstride=rstride, facecolors=grid_map, shade=shade) if savefig: plt.savefig(filename) else: plt.show() @u.quantity_input(inclination=u.deg) def view(self, inclination=90*u.deg, phase=0.0, what='fluxes', projection='mollweide', cmap='magma', savefig=False, filename='star_surface.png', dlat=30, dlon=30, **kwargs): rot = (360*phase, 90-inclination.to(u.deg).value, 0) if what == 'fluxes': vals = self.tile_fluxes * self.tile_scales vals = vals / vals.max() elif what == 'areas': vals = self.tile_areas / self.tile_areas.max() if 'mollweide'.find(projection) == 0: hp.mollview(vals, rot=rot, cmap=cmap, **kwargs) elif 'cartesian'.find(projection) == 0: hp.cartview(vals, rot=rot, cmap=cmap, **kwargs) elif 'orthographic'.find(projection) == 0: hp.orthview(vals, rot=rot, cmap=cmap, **kwargs) else: raise ValueError('Unrecognised projection') hp.graticule(dlat, dlon) if savefig: plt.savefig(filename) else: plt.show() @u.quantity_input(inclination=u.deg) def _luminosity_array(self, phase, inclination): if self.tile_locs is None: self.setup_grid() # get CartesianRepresentation pointing to earth at these phases earth = set_earth(inclination, phase) mu = dot(earth, self.tile_dirs, normalise=True) # mask of visible elements mask = mu >= 0.0 # broadcast and calculate phase = np.asarray(phase) new_shape = phase.shape + self.tile_fluxes.shape assert(new_shape == mu.shape), "Broadcasting has gone wrong" fluxes = np.tile(self.tile_fluxes, phase.size).reshape(new_shape) scales = np.tile(self.tile_scales, phase.size).reshape(new_shape) areas = np.tile(self.tile_areas, phase.size).reshape(new_shape) # limb darkened sum of all tile fluxes lum = (fluxes * scales * (1.0 - self.ulimb + np.fabs(mu)*self.ulimb) * areas * mu) # no contribution from invisible tiles lum[mask] = 0.0 return lum @u.quantity_input(inclination=u.deg) def calc_luminosity(self, phase, inclination): lum = self._luminosity_array(phase, inclination) return np.sum(lum, axis=1) @u.quantity_input(inclination=u.deg) @u.quantity_input(v_macro=u.km/u.s) @u.quantity_input(v_inst=u.km/u.s) @u.quantity_input(v_min=u.km/u.s) @u.quantity_input(v_max=u.km/u.s) def calc_line_profile(self, phase, inclination, nbins=100, v_macro=2*u.km/u.s, v_inst=4*u.km/u.s, v_min=-40*u.km/u.s, v_max=40*u.km/u.s): # get CartesianRepresentation pointing to earth at these phases earth = set_earth(inclination, phase) # get CartesianRepresentation of projected velocities vproj = dot(earth, self.tile_velocities).to(u.km/u.s) # which tiles fall in which bin? bins = np.linspace(v_min, v_max, nbins) indices = np.digitize(vproj, bins) lum = self._luminosity_array(phase, inclination) phase = np.asarray(phase) trailed_spectrum = np.zeros((phase.size, nbins)) for i in range(nbins): mask = (indices == i) trailed_spectrum[:, i] = np.sum(lum*mask, axis=1) # convolve with instrumental and local line profiles # TODO: realistic Line Profile Treatment # For now we assume every element has same intrinsic # line profile bin_width = (v_max-v_min)/(nbins-1) profile_width_in_bins = np.sqrt(v_macro**2 + v_inst**2) / bin_width gauss_kernel = Gaussian1DKernel(stddev=profile_width_in_bins, mode='linear_interp') for i in range(phase.size): trailed_spectrum[i, :] = convolve(trailed_spectrum[i, :], gauss_kernel, boundary='extend') return bins, trailed_spectrum
def __init__(self, center, length=None, angle=None, sample=2, width=None): # Check input types if not isinstance(center, (SkyCoord, BaseCoordinateFrame)): raise TypeError( "The central position should be given as a SkyCoord object") if not isinstance(length, u.Quantity) or not length.unit.is_equivalent( u.deg): raise TypeError( "The length should be given as an angular Quantity") if not isinstance(angle, u.Quantity) or not angle.unit.is_equivalent( u.deg): raise TypeError("The angle should be given as an angular Quantity") # We set up the path by adding and removing half the length along the # declination axis, then rotate the resulting two points around the # center. # Convert the central position to cartesian coordinates c1, c2, c3 = center.cartesian.xyz.value # Find the end points of the path clon, clat = center.spherical.lon, center.spherical.lat try: plat = clat + np.linspace(-length * 0.5, length * 0.5, sample) except ValueError: # Numpy 1.10+ plat = clat + np.linspace(-length.value * 0.5, length.value * 0.5, sample) * length.unit x, y, z = UnitSphericalRepresentation(clon, plat).to_cartesian().xyz.value # Rotate around central point # Because longitude increases to the left, we have to take -angle angle = -angle # We rotate (x,y,z) around (c1,c2,c3) by making use of the following # equations: # # x' = x cos a + (1 - cos a)(c1c1x + c1c2y + c1c3z) + (c2z - c3y)sin a # y' = y cos a + (1 - cos a)(c2c1x + c2c2y + c2c3z) + (c3x - c1z)sin a # z' = z cos a + (1 - cos a)(c3c1x + c3c2y + c3c3z) + (c1y - c2x)sin a # # Source: https://www.uwgb.edu/dutchs/MATHALGO/sphere0.htm cosa = np.cos(angle) sina = np.sin(angle) xd = x * cosa + (1 - cosa) * (c1 * c1 * x + c1 * c2 * y + c1 * c3 * z) + (c2 * z - c3 * y) * sina yd = y * cosa + (1 - cosa) * (c2 * c1 * x + c2 * c2 * y + c2 * c3 * z) + (c3 * x - c1 * z) * sina zd = z * cosa + (1 - cosa) * (c3 * c1 * x + c3 * c2 * y + c3 * c3 * z) + (c1 * y - c2 * x) * sina # Construct representations points = center.realize_frame(CartesianRepresentation(x=xd, y=yd, z=zd)) super(PathFromCenter, self).__init__(points, width=width)
return frame(*args) elif kwargs: return frame(**kwargs) """ These are common 2D params, kwargs are frame specific """ two_D_parameters = [([0 * u.deg, 0 * u.arcsec], None), ([0 * u.deg, 0 * u.arcsec], { 'obstime': '2011/01/01T00:00:00' }), ([0 * u.deg, 0 * u.arcsec], { 'representation': 'unitspherical' }), ([UnitSphericalRepresentation(0 * u.deg, 0 * u.arcsec)], None), ([UnitSphericalRepresentation(0 * u.deg, 0 * u.arcsec)], None), ([UnitSphericalRepresentation(0 * u.deg, 0 * u.arcsec)], { 'obstime': '2011/01/01T00:00:00' })] """ These are common 3D params, kwargs are frame specific """ three_D_parameters = [ ([0 * u.deg, 0 * u.arcsec, 1 * u.Mm], None), ([0 * u.deg, 0 * u.arcsec, 1 * u.Mm], { 'obstime': '2011/01/01T00:00:00' }), ([0 * u.deg, 0 * u.arcsec, 1 * u.Mm], { 'representation': 'spherical' }), ([SphericalRepresentation(0 * u.deg, 0 * u.arcsec, 1 * u.Mm)], None),
def init_frame(frame, args, kwargs): if args and kwargs: return frame(*args, **kwargs) elif args: return frame(*args) elif kwargs: return frame(**kwargs) """ These are common 2D params, kwargs are frame specific """ two_D_parameters = [ ([0 * u.deg, 0 * u.arcsec], None), ([0 * u.deg, 0 * u.arcsec], {'obstime': '2011/01/01T00:00:00'}), ([UnitSphericalRepresentation(0 * u.deg, 0 * u.arcsec)], None), ([UnitSphericalRepresentation(0 * u.deg, 0 * u.arcsec)], {'obstime': '2011/01/01T00:00:00'}), ([0 * u.deg, 0 * u.arcsec], {'representation_type': 'unitspherical'}) ] """ These are common 3D params, kwargs are frame specific """ three_D_parameters = [ ([0 * u.deg, 0 * u.arcsec, 1 * u.Mm], None), ([0 * u.deg, 0 * u.arcsec, 1 * u.Mm], {'obstime': '2011/01/01T00:00:00'}), ([0 * u.deg, 0 * u.arcsec, 1 * u.Mm], {'representation_type': 'spherical'}), ([SphericalRepresentation(0 * u.deg, 0 * u.arcsec, 1 * u.Mm)], None), ([SphericalRepresentation(0 * u.deg, 0 * u.arcsec, 1 * u.Mm)], None), ( [SphericalRepresentation(0 * u.deg, 0 * u.arcsec, 1 * u.Mm)], {'obstime': '2011/01/01T00:00:00'})
def pixel_to_skycoord(xp, yp, wcs, origin=0, mode='all', cls=None): """ Convert a set of pixel coordinates into a `~astropy.coordinates.SkyCoord` coordinate. Parameters ---------- xp, yp : float or `numpy.ndarray` The coordinates to convert. wcs : `~astropy.wcs.WCS` The WCS transformation to use. origin : int Whether to return 0 or 1-based pixel coordinates. mode : 'all' or 'wcs' Whether to do the transformation including distortions (``'all'``) or only including only the core WCS transformation (``'wcs'``). cls : class or None The class of object to create. Should be a `~astropy.coordinates.SkyCoord` subclass. If None, defaults to `~astropy.coordinates.SkyCoord`. Returns ------- coords : Whatever ``cls`` is (a subclass of `~astropy.coordinates.SkyCoord`) The celestial coordinates See Also -------- astropy.coordinates.SkyCoord.from_pixel """ # Import astropy.coordinates here to avoid circular imports from astropy.coordinates import SkyCoord, UnitSphericalRepresentation # we have to do this instead of actually setting the default to SkyCoord # because importing SkyCoord at the module-level leads to circular # dependencies. if cls is None: cls = SkyCoord if _has_distortion(wcs) and wcs.naxis != 2: raise ValueError( "Can only handle WCS with distortions for 2-dimensional WCS") # Keep only the celestial part of the axes, also re-orders lon/lat wcs = wcs.sub([WCSSUB_LONGITUDE, WCSSUB_LATITUDE]) if wcs.naxis != 2: raise ValueError("WCS should contain celestial component") # Check which frame the WCS uses frame = wcs_to_celestial_frame(wcs) # Check what unit the WCS gives lon_unit = u.Unit(wcs.wcs.cunit[0]) lat_unit = u.Unit(wcs.wcs.cunit[1]) # Convert pixel coordinates to celestial coordinates if mode == 'all': lon, lat = wcs.all_pix2world(xp, yp, origin) elif mode == 'wcs': lon, lat = wcs.wcs_pix2world(xp, yp, origin) else: raise ValueError("mode should be either 'all' or 'wcs'") # Add units to longitude/latitude lon = lon * lon_unit lat = lat * lat_unit # Create a SkyCoord-like object data = UnitSphericalRepresentation(lon=lon, lat=lat) coords = cls(frame.realize_frame(data)) return coords
def plot_reachable_zone( ax: matplotlib.axes.Axes, mount_model: MountModel, axis_0_west_limit: float = 110, axis_0_east_limit: float = 110, ) -> None: """Plot area(s) of sky reachable by the mount. This only accounts for what area of the sky is reachable. It does not take into account whether the mount can keep up with a specific target's trajectory. Some trajectories, especially those that pass near the mount pole, may require slew rates that exceed what the mount is capable of. This function assumes an equatorial mount with limits on the right ascension axis. Args: ax: Axes object this function will plot on. This should be generated by `make_sky_plot()`. mount_model: Mount model from which this plot will be generated. axis_0_west_limit: Western limit on axis 0 in degrees from the meridian. axis_0_east_limit: Eastern limit on axis 0 in degrees from the meridian. """ if axis_0_west_limit < 90 or axis_0_east_limit < 90: # current logic would not shade the correct regions of the polar plot raise ValueError('Axis limits less than 90 degrees from meridian are not supported') # convert from arg values to encoder position angles axis_0_west_limit = 180 - axis_0_west_limit axis_0_east_limit = 180 + axis_0_east_limit # place a dot at the position of the mount pole mount_pole_topo = mount_model.spherical_to_topocentric( UnitSphericalRepresentation( lon=0*u.deg, lat=90*u.deg, ) ) ax.plot(np.radians(mount_pole_topo.az.deg), 90.0 - mount_pole_topo.alt.deg, 'k.') for meridian_side in MeridianSide: if meridian_side == MeridianSide.EAST: color = 'blue' legend_label = 'east of mount meridian' axis_1_range = np.linspace(0, 180, 100) + mount_model.model_params.axis_1_offset.deg az = np.linspace(mount_pole_topo.az.deg, mount_pole_topo.az.deg + 180, 100) else: axis_1_range = np.linspace(180, 360, 100) + mount_model.model_params.axis_1_offset.deg color = 'red' legend_label = 'west of mount meridian' az = np.linspace(mount_pole_topo.az.deg - 180, mount_pole_topo.az.deg, 100) # add a circle patch outside the visible area of the plot purely for the purpose of # generating an entry in the legend for this region ax.add_patch(Circle((0, 100), radius=0, color=color, alpha=0.2, label=legend_label)) alt = 90*np.ones_like(az) fill_to_horizon(ax, az, alt, color=color) for axis_0 in (axis_0_west_limit, axis_0_east_limit): az = [] alt = [] for axis_1 in axis_1_range: topo = mount_model.encoders_to_topocentric( MountEncoderPositions( Longitude(axis_0*u.deg), Longitude(axis_1*u.deg), ) ) az.append(topo.az.deg) alt.append(topo.alt.deg) az = np.array(az) alt = np.array(alt) ax.plot(np.radians(az), 90.0 - alt, ':', color=color) if axis_0 == axis_0_east_limit and meridian_side == MeridianSide.EAST: fill_to_horizon(ax, az, alt, color=color) elif axis_0 == axis_0_west_limit and meridian_side == MeridianSide.EAST: fill_to_zenith(ax, az, alt, color=color) elif axis_0 == axis_0_east_limit and meridian_side == MeridianSide.WEST: fill_to_zenith(ax, az, alt, color=color) elif axis_0 == axis_0_west_limit and meridian_side == MeridianSide.WEST: fill_to_horizon(ax, az, alt, color=color)