Source code for lvmsurveysim.utils.shadow_height_lib

#!/usr/bin/python3
import numpy as np
import sys
from astropy import units as u
from skyfield.api import load
from skyfield.api import Topos

[docs]class shadow_calc(object): def __init__(self, observatory_name="LCO", observatory_elevation=2380.0*u.m, observatory_lat='29.0146S', observatory_lon = '70.6926W', jd=2459458, eph=None, earth=None, sun=None): """ Initialization sets a default observatory to LCO, and the default time to the date when initialized. """ super().__init__() # Load the ephemeral datat for the earth and sun. if eph is None: self.eph = load('de421.bsp') # Get functions for the earth, sun and observatory self.earth = earth if self.earth is None: self.earth = self.eph['earth'] self.sun = sun if self.sun is None: self.sun = self.eph['sun'] self.observatory_elevation = observatory_elevation try: self.observatory_elevation.to("m") except: sys.exit("Observatory elevation does not have unit of length") self.observatory_name = observatory_name self.observatory_lat = observatory_lat self.observatory_lon = observatory_lon self.observatory_topo = Topos(observatory_lat, observatory_lon, elevation_m=self.observatory_elevation.to("m").value) self.ts = load.timescale() """ These are going to be vectors which are the x,y,z positions of the puzzle. Credit for this solution to the intersection goes to: Julien Guertault @ http://lousodrome.net/blog/light/2017/01/03/intersection-of-a-ray-and-a-cone/ """ self.ra = None self.dec = None self.pointing_unit_vectors = None self.xyz_earth = None self.xyz_sun = None self.xyz_observatory = None self.v = None self.xyz_c = None self.a = None self.b = None self.c = None self.delta = None self.dist = None self.heights = None # Those are very self explanitory self.earth_radius=6.357e6*u.m self.sun_radius=695.700e6*u.m # Distance from Sun to the earth. self.d_se = 1*u.au # Distance from tip of the shadow cone to the earth self.d_ec = self.d_se * (self.earth_radius / self.sun_radius) # Opening angle of the shadow cone self.shadow_cone_theta = np.arctan(self.earth_radius/self.d_ec) self.shadow_cone_cos_theta_sqr = np.square(np.cos(self.shadow_cone_theta)) # Finally, updated the xyz coordinate vectors of earth, sun and observatory. self.jd = jd if jd is not None: self.t = self.ts.tt_jd(jd) else: self.t = self.ts.now() self.update_time(jd)
[docs] def cone_ra_dec(self): # This returns the cone RA,Dec for testing. # It has a shadow height that is analyically easy to calculate observatory_to_c = self.xyz_c - self.xyz_observatory # Leave dec in steradian for just a moment dec = np.arctan(observatory_to_c[2]/np.sqrt(np.square(observatory_to_c[0])+np.square(observatory_to_c[1]))) # Use dec in steradians to solve for ra. ra = np.degrees(np.arccos(observatory_to_c[0]/ (self.vecmag(observatory_to_c) * np.cos(dec)))) #Now convert to degree dec = np.degrees(dec) return ra, dec
[docs] def set_coordinates(self, ra, dec): # Set coordinates and calculate unit vectors. ra = np.atleast_1d(ra) dec = np.atleast_1d(dec) self.ra = ra self.dec = dec self.pointing_unit_vectors = np.zeros(shape=(len(self.ra),3)) a = np.pi / 180. self.pointing_unit_vectors[:,0] = np.cos(self.ra*a)*np.cos(self.dec*a) self.pointing_unit_vectors[:,1] = np.sin(self.ra*a)*np.cos(self.dec*a) self.pointing_unit_vectors[:,2] = np.sin(self.dec*a)
[docs] def update_time(self, jd=None): """ Update the time, and all time dependent vectors """ if jd is not None: self.jd = jd self.t = self.ts.tt_jd(self.jd) self.update_xyz() self.update_vec_c() self.co = self.xyz_c - self.xyz_observatory
[docs] def update_xyz(self): """ Due to the nature of the vectors, it is not possible to combine the earth and the observatory topo directly. Therefore this requires the calculation of an observatory position to be done using the XYZ positions defined at a particular time, and with a particular unit. To prevent unit issues later from arising SI units are adopted. In the case where other units are desired, like plotting the solar system, local conversions can be done. """ self.xyz_earth = self.earth.at(self.t).position.au self.xyz_sun = self.sun.at(self.t).position.au self.xyz_observatory = self.earth.at(self.t).position.au + self.observatory_topo.at(self.t).position.au
[docs] def update_vec_c(self): """ d_se : distance sun to earth d_ec : distance earth to (c)one tip c_unit_vec: unit vector from cone to earth, or earth to sun. """ # We reverse the vector sun to earth explicitly with -1.0 for clarity self.v = -1.0*(self.xyz_earth - self.xyz_sun)/np.sqrt(np.sum(np.square(self.xyz_earth-self.xyz_sun))) self.xyz_c = self.xyz_earth - self.v * self.d_ec.to("au").value
[docs] def solve_for_height(self, mask=None, unit="km"): if mask is None: mask = np.full(len(self.pointing_unit_vectors), True) puv = self.pointing_unit_vectors[mask] self.a = np.square(np.sum(puv*self.v,axis=1)) - self.shadow_cone_cos_theta_sqr self.b = 2*( np.sum(puv*self.v, axis=1) * np.sum(self.co*self.v) - np.sum(puv*self.co, axis=1)*self.shadow_cone_cos_theta_sqr) self.c = np.square(np.sum(self.co*self.v)) - np.sum(self.co * self.co) * self.shadow_cone_cos_theta_sqr self.delta = np.square(self.b) - 4 * self.a * self.c self.dist = np.full(len(self.a), np.nan) dist_b1 = np.full(len(self.a), np.nan) dist_b2 = np.full(len(self.a), np.nan) # Get P distance to point. positive_delta = self.delta >= 0 dist_b1[positive_delta] = -1*(-self.b[positive_delta]+np.sqrt(self.delta[positive_delta])) / (2*self.a[positive_delta]) dist_b2[positive_delta] = -1*(-self.b[positive_delta]-np.sqrt(self.delta[positive_delta])) / (2*self.a[positive_delta]) dist_b1[dist_b1 < 0.0] = np.nan dist_b2[dist_b2 < 0.0] = np.nan self.dist[positive_delta] = np.nanmin([dist_b1[positive_delta], dist_b2[positive_delta]], axis=0) # caluclate xyz of each ra, using the distance to the shadow intersection and the normal vector form the observatory pointing_xyz = puv * self.dist[:,np.newaxis] + self.xyz_observatory self.heights = (self.vecmag(pointing_xyz - self.xyz_earth)*u.au - self.earth_radius).to(unit).value
[docs] def get_heights(self, mask=None, jd=None, return_heights=True,unit=u.km): if jd is not None: self.jd = jd self.update_time() self.solve_for_height(mask=mask, unit=unit) if return_heights: return self.heights
[docs] def vecmag(self, a): """ Return the magnitude of a set of vectors around an abritrary origin """ if len(np.shape(a)) == 1: return np.sqrt(np.square(a[0]) + np.square(a[1]) + np.square(a[2])) else: return np.sqrt(np.square(a[:, 0]) + np.square(a[:, 1]) + np.square(a[:, 2]))
[docs]class orbit_animation(object): def __init__(self, calculator, jd0=None, djd=1/24.): if jd0 is None: self.jd0 = calculator.jd else: self.jd0 = jd0 self.djd = djd self.calculator = calculator self.calculator.observatory_zenith_topo = Topos(calculator.observatory_lat, calculator.observatory_lon, elevation_m=self.calculator.earth_radius.to("m").value*10) import matplotlib.pyplot as plt import matplotlib.animation as animation import matplotlib.patches as patches self.plt = plt self.animation = animation self.patches = patches self.fig, self.ax = self.plt.subplots(figsize=(50,50))
[docs] def init_plotting_animation(self): self.line1, = self.ax.plot([], [], 'ro', lw=2) self.line2, = self.ax.plot([], [], '.-', lw=5) self.line3, = self.ax.plot([], [], '.-', lw=5) self.path, = self.ax.plot([],[],'-', lw=1) self.ax.grid() self.xdata, self.ydata = [], [] self.pathx, self.pathy = [], [] self.ax.set_ylim(-150 * self.calculator.earth_radius.to("au").value, 150 * self.calculator.earth_radius.to("au").value) self.ax.set_xlim(-150 * self.calculator.earth_radius.to("au").value, 150 * self.calculator.earth_radius.to("au").value) del self.xdata[:] del self.ydata[:] self.line1.set_data(self.xdata, self.ydata) self.line2.set_data(self.xdata, self.ydata)
[docs] def do_animation(self, N_days=1.0): ani = self.animation.FuncAnimation(self.fig, self.animation_update_positions, frames=(24*N_days), repeat_delay=0, blit=False, interval=100, init_func=self.init_plotting_animation, repeat=0) ani.save("/tmp/im.mp4")
[docs] def snap_shot(self,jd,ra,dec, show=True, extra=False): self.init_plotting_animation() self.calculator.set_coordinates(np.array([ra]),np.array([dec])) self.calculator.update_time(jd) self.calculator.xyz_observatory_zenith = self.calculator.xyz_earth + self.calculator.observatory_zenith_topo.at(self.calculator.t).position.au height_au = 3.0 * self.calculator.earth_radius.to("au").value self.ax.set_xlim( ( self.calculator.xyz_earth[0] - height_au * 160, self.calculator.xyz_earth[0] + height_au * 160 ) ) self.ax.set_ylim( ( self.calculator.xyz_earth[1] - height_au * 90, self.calculator.xyz_earth[1] + height_au * 90 ) ) circ = self.patches.Circle((self.calculator.xyz_earth[0], self.calculator.xyz_earth[1]), self.calculator.earth_radius.to( "au" ).value, alpha=0.8, fc='yellow') self.ax.add_patch( circ ) los_xyz = self.calculator.xyz_observatory + self.calculator.d_ec.to("au").value * self.calculator.pointing_unit_vectors[0] #self.line1.set_data( (x_heights * u.m).to( "au" ).value, ( y_heights * u.m ).to("au").value ) self.line1.set_data( [ self.calculator.xyz_c[0] ], [ self.calculator.xyz_c[1] ] ) self.line2.set_data( [ self.calculator.xyz_observatory_zenith[0] ], [ self.calculator.xyz_observatory_zenith[1] ] ) self.line3.set_data( [ self.calculator.xyz_observatory[0] ] , [ self.calculator.xyz_observatory[1] ] ) self.ax.plot([self.calculator.xyz_c[0], self.calculator.xyz_sun[0]],[self.calculator.xyz_c[1], self.calculator.xyz_sun[1]], alpha=0.5) self.ax.plot([los_xyz[0], self.calculator.xyz_observatory[0]],[los_xyz[1], self.calculator.xyz_observatory[1]], c="k", alpha=0.5) if extra is not False: #This will plot a line using arrays provided in the extra bin. self.ax.plot(extra[0], extra[1]) self.plt.gca().set_aspect('equal', adjustable='box') if show: self.plt.show() else: return self.plt
[docs] def animation_update_positions(self, frame): if len( self.ax.patches ) > 1: del( self.ax.patches[-1] ) jd = self.jd0 + frame*self.djd #self.calculator.t = self.calculator.ts.tt_jd( self.jd0 + frame*self.djd) self.calculator.update_time(jd) self.calculator.xyz_observatory_zenith = self.calculator.xyz_earth + self.calculator.observatory_zenith_topo.at(self.calculator.t).position.au height_au = 3.0 * self.calculator.earth_radius.to("au").value ###################################################################### # The next set of lines calculates shadow heights. That's for later # ###################################################################### # N_ra = 24 # N_dec = 9 # x_heights = np.zeros(len(N_ra * N_dec)+1) # y_heights = np.zeros(len(N_ra * N_dec)+1) # # for ra_i, ra in enumerate(np.linspace(1,24,N_ra)): # for dec_j, dec in enumerate(np.linspace(-90, 0, num=N_dec, endpoint=True)): # # for height in heights: # self.calculator.point_along_ray = position_from_radec(ra, dec, distance=height_au, epoch=None, t=self.calculator.t, center=self.calculator.observatory_topo, target=None) #, observer_data=LCO_topo.at(t).observer_data) # self.calculator.xyz_along_ray = self.calculator.xyz_observatory + self.calculator.point_along_ray.position.au # x_heights[ ra_i + dec_j*N_ra ] = self.calculator.xyz_along_ray[0] # y_heights[ ra_i + dec_j*N_ra ] = self.calculator.xyz_along_ray[1] # # x_heights[-1] = self.calculator.xyz_earth[0] # y_heights[-1] = self.calculator.xyz_earth[1] ###################################################################### self.ax.set_xlim( ( self.calculator.xyz_earth[0] - height_au * 150, self.calculator.xyz_earth[0] + height_au * 150 ) ) self.ax.set_ylim( ( self.calculator.xyz_earth[1] - height_au * 150, self.calculator.xyz_earth[1] + height_au * 150 ) ) circ = self.patches.Circle((self.calculator.xyz_earth[0], self.calculator.xyz_earth[1]), self.calculator.earth_radius.to( "au" ).value, alpha=0.8, fc='yellow') self.ax.add_patch( circ ) #self.line1.set_data( (x_heights * u.m).to( "au" ).value, ( y_heights * u.m ).to("au").value ) self.line1.set_data( [ self.calculator.xyz_c[0] ], [ self.calculator.xyz_c[1] ] ) self.line2.set_data( [ self.calculator.xyz_observatory_zenith[0] ], [ self.calculator.xyz_observatory_zenith[1] ] ) self.line3.set_data( [ self.calculator.xyz_observatory[0] ] , [ self.calculator.xyz_observatory[1] ] ) self.pathx.append( self.calculator.xyz_earth[0] ) self.pathy.append( self.calculator.xyz_earth[1] ) self.path.set_data( self.pathx, self.pathy )
#line2.set_data([sun.at(t).position.au[0]],[sun.at(t).position.au[1]])
[docs]def test_shadow_calc(): jd = 2459458.5+20 + (4.75)/24. # Initiate tests. test_results ={} try: test = "Create Class" calculator = shadow_calc() test_results[test] = "Pass" except: test_results[test] = "Fail" orbit_ani = orbit_animation(calculator) orbit_ani.calculator.update_time(jd) ra, dec = orbit_ani.calculator.cone_ra_dec() orbit_ani.snap_shot(jd=jd, ra=ra, dec=dec) # orbit_ani.do_animation() compare_old = True if compare_old: eph = load('de421.bsp') import lvmsurveysim.utils.iterative_shadow_height_lib as iterative_shadow_height_lib iter_calc = iterative_shadow_height_lib.shadow_calc(observatory_name='LCO', observatory_elevation=2380*u.m, observatory_lat='29.0146S', observatory_lon='70.6926W', eph=eph, earth=eph['earth'], sun=eph['sun']) iter_calc.update_t(jd) old_h = iter_calc.height_from_radec(ra/15., dec, simple_output=True)['height'] new_h = calculator.get_heights(return_heights=True, unit="m") print("old: %f; new: %f"%(old_h, new_h)) from astropy.coordinates import SkyCoord from astropy.coordinates import Angle, Latitude, Longitude from astropy import units as u try: test = "Shadow RA/DEC" ra, dec = calculator.cone_ra_dec() except: test_results[test] = "Fail" try: test = "Set Coordinates" calculator.set_coordinates(np.array([ra]), np.array([dec]) ) test_results[test] = "Pass" except: test_results[test] = "Fail" try: test = "set jd=2459458.5" jd = 2459458.5+20 + 4.75/24. calculator.update_time(jd) test_results[test] = "Pass" except: test_results[test] = "Fail" try: test = "Run" calculator.get_heights() test_results[test] = "Pass" except: test_results[test] = "Fail" try: test = "loop" for jd in np.linspace(jd, jd+1, 1/24.): calculator.update_time(jd) heights = calculator.get_heights(return_heights=True, unit="km") #print("height_v(jd=%f) = %f"%(jd, heights[0])) test_results[test] = "Pass" except: test_results[test] = "Fail" for test in test_results.keys(): print ("%s -> %s"%(test, test_results[test]))
if __name__ == "__main__": test_shadow_calc()