def semi_circular_loop(length: u.m, theta0: u.deg = 0 * u.deg): """ Return a Heliographic Stonyhurst coordinate object with points of a semi circular loop in it. """ r_sun = const.R_sun def r_2_func(x): return np.arccos(0.5 * x / r_sun.to(u.cm).value) - np.pi + length.to( u.cm).value / 2. / x r_2 = scipy.optimize.bisect(r_2_func, length.to(u.cm).value / (2 * np.pi), length.to(u.cm).value / np.pi) * u.cm alpha = np.arccos(0.5 * (r_2 / r_sun).decompose()) phi = np.linspace(-np.pi * u.rad + alpha, np.pi * u.rad - alpha, 2000) # Quadratic formula to find r a = 1. b = -2 * (r_sun.to(u.cm) * np.cos(phi.to(u.radian))) c = r_sun.to(u.cm)**2 - r_2.to(u.cm)**2 r = (-b + np.sqrt(b**2 - 4 * a * c)) / 2 / a # Choose only points above the surface i_r = np.where(r > r_sun) r = r[i_r] phi = phi[i_r] hcc_frame = frames.Heliocentric( observer=SkyCoord(lon=0 * u.deg, lat=theta0, radius=r_sun, frame='heliographic_stonyhurst')) return SkyCoord(x=r.to(u.cm) * np.sin(phi.to(u.radian)), y=u.Quantity(r.shape[0] * [0 * u.cm]), z=r.to(u.cm) * np.cos(phi.to(u.radian)), frame=hcc_frame).transform_to('heliographic_stonyhurst')
def semi_circular_loop(length: u.m, theta0: u.deg=0*u.deg): """ Return a Heliographic Stonyhurst coordinate object with points of a semi circular loop in it. """ r_sun = const.R_sun def r_2_func(x): return np.arccos(0.5 * x / r_sun.to(u.cm).value) - np.pi + length.to(u.cm).value / 2. / x r_2 = scipy.optimize.bisect(r_2_func, length.to(u.cm).value / (2 * np.pi), length.to(u.cm).value / np.pi) * u.cm alpha = np.arccos(0.5 * (r_2 / r_sun).decompose()) phi = np.linspace(-np.pi * u.rad + alpha, np.pi * u.rad - alpha, 2000) # Quadratic formula to find r a = 1. b = -2 * (r_sun.to(u.cm) * np.cos(phi.to(u.radian))) c = r_sun.to(u.cm)**2 - r_2.to(u.cm)**2 r = (-b + np.sqrt(b**2 - 4 * a * c)) / 2 / a # Choose only points above the surface i_r = np.where(r > r_sun) r = r[i_r] phi = phi[i_r] hcc_frame = frames.Heliocentric( observer=SkyCoord(lon=0 * u.deg, lat=theta0, radius=r_sun, frame='heliographic_stonyhurst')) return SkyCoord( x=r.to(u.cm) * np.sin(phi.to(u.radian)), y=u.Quantity(r.shape[0] * [0 * u.cm]), z=r.to(u.cm) * np.cos(phi.to(u.radian)), frame=hcc_frame).transform_to('heliographic_stonyhurst')
def fwd(self, lat: u.deg, lon: u.deg, azimuth: u.deg, distance: u.m): """Solve forward geodetic problem. Returns latitudes, longitudes and back azimuths of terminus points given latitudes `lat` and longitudes `lon` of initial points, plus forward `azimuth`s and `distance`s. This method use `pyproj.Geod.fwd` as a backend. Parameters ---------- lat : ~astropy.units.Quantity Geodetic latitude of the initial point. lon : ~astropy.units.Quantity Longitude of the initial point. azimuth : ~astropy.units.Quantity Geodetic azimuth. distance : ~astropy.units.Quantity Distance. Returns ------- lat : ~astropy.units.Quantity Geodetic latitude of the terminus point. lon : ~astropy.units.Quantity Longitude of the terminus point. back_azimuth : ~astropy.units.Quantity Back geodetic azimuth. """ out_lon, out_lat, out_baz = self.geod.fwd(lon.to('radian').value, lat.to('radian').value, azimuth.to('radian').value, distance.to('m').value, radians=True) return out_lat * u.rad, out_lon * u.rad, out_baz * u.rad
def enu_to_ecef(x: u.m, y: u.m, z: u.m, origin: tuple[u.deg, u.deg, u.m], ell=None): """Convert local cartesian to geocentric cartesian coordinates. Convert local east-north-up (ENU) local cartesian coordinate system with the origin in (`lat0`, `lon0`, `height0`) to the Earth Centered Earth Fixed (ECEF) cartesian coordinates. Parameters ---------- x, y, z : ~astropy.units.Quantity Local east-north-uo cartesian coordinates. origin : tuple of ~astropy.units.Quantity Ggeocentric (spherical) or geodetic coordinates of the origin (`lat0`, `lon0`, `r0`) or (`lat0`, `lon0`, `h0`). ell : instance of the `pygeoid.coordinates.ellipsoid.Ellipsoid` Reference ellipsoid to which geodetic coordinates are referenced to. Default is None, meaning spherical coordinates instead of geodetic. Returns ------- x, y, z : ~astropy.units.Quantity Geocentric cartesian coordinates, in metres. """ rotation_matrix = _ecef_to_enu_rotation_matrix(origin[0], origin[1]).T out_shape = x.shape x, y, z = _np.dot( rotation_matrix, _np.array([ _np.asarray(x.to('m').value).flatten(), _np.asarray(y.to('m').value).flatten(), _np.asarray(z.to('m').value).flatten() ])) * u.m if ell is None: x0, y0, z0 = spherical_to_cartesian(*origin) else: x0, y0, z0 = geodetic_to_cartesian(*origin, ell=ell) return ((x + x0).reshape(out_shape), (y + y0).reshape(out_shape), (z + z0).reshape(out_shape))
def rms(diameter: u.m, wavelength: u.nm, fried_param: u.m, outer_scale: u.m, freqs=None): ''' Von Karman wavefront rms value over a circular aperture Parameters ---------- diameter: `~astropy.units.quantity.Quantity` equivalent to meter Aperture diameter wavelength: `~astropy.units.quantity.Quantity` equivalent to nanometer wavelength fried_param: `~astropy.units.quantity.Quantity` equivalent to meter Fried parameter r0 defined at the specified wavelength outer_scale: `~astropy.units.quantity.Quantity` equivalent to meter Outer scale L0. Use np.inf for Kolmogorov spectrum Other Parameters ---------- freqs: array of `~astropy.units.quantity.Quantity` equivalent to 1/meter spatial frequencies array. Default logspace(-8, 4, 1000) m^-1 Returns ------- rms: `~astropy.units.quantity.Quantity` equivalent to nm wavefront rms for the specified von Karman turbulence ''' R = 0.5 * diameter.to(u.m).value wl = wavelength.to(u.nm).value r0 = fried_param.to(u.m).value L0 = outer_scale.to(u.m).value psd = VonKarmanPsd(r0, L0) if freqs is None: freqs = np.logspace(-8, 4, 1000) / u.m freqs = freqs.to(1 / u.m).value bess = jv(1, 2 * np.pi * R * freqs) psdTotal = psd.spatial_psd(freqs) psdPistonRem = psdTotal * (1 - (bess / (np.pi * R * freqs))**2) varInRad2 = np.trapz(psdPistonRem * 2 * np.pi * freqs, freqs) return np.sqrt(varInRad2) * wl / 2 / np.pi * u.nm
def __init__(self, normal, center: u.m, radius: u.m, current: u.A, n=300): self.normal = normal / np.linalg.norm(normal) self.center = center.to(u.m).value if radius > 0: self.radius = radius.to(u.m).value else: raise ValueError("Radius should bu larger than 0") self.current = current.to(u.A).value # parametric equation # find other two axises in the disc plane z = np.array([0, 0, 1]) axis_x = np.cross(z, self.normal) axis_y = np.cross(self.normal, axis_x) if np.linalg.norm(axis_x) == 0: axis_x = np.array([1, 0, 0]) axis_y = np.array([0, 1, 0]) else: axis_x = axis_x / np.linalg.norm(axis_x) axis_y = axis_y / np.linalg.norm(axis_y) self.axis_x = axis_x self.axis_y = axis_y def curve(t): if isinstance(t, np.ndarray): t = np.expand_dims(t, 0) axis_x_mat = np.expand_dims(axis_x, 1) axis_y_mat = np.expand_dims(axis_y, 1) return self.radius*(np.matmul(axis_x_mat, np.cos(t)) + np.matmul(axis_y_mat, np.sin(t))) \ + np.expand_dims(self.center, 1) else: return self.radius * (np.cos(t) * axis_x + np.sin(t) * axis_y) + self.center self.curve = curve self.roots_legendre = roots_legendre(n) self.n = n
def cartesian_to_geodetic(x: u.m, y: u.m, z: u.m, ell): """Convert 3D cartesian to geodetic coordinates. Parameters ---------- x, y, z : ~astropy.units.Quantity Cartesian coordinates. ell : instance of the `pygeoid.coordinates.ellipsoid.Ellipsoid` Reference ellipsoid to which geodetic coordinates are referenced to. Returns ------- lat : ~astropy.units.Quantity Geodetic latitude. lon : ~astropy.units.Quantity Geodetic longitude. height : float or array_like of floats Geodetic height. Notes ----- The algorithm of H. Vermeille is used for this transformation [1]_. References ---------- .. [1] Vermeille, H., 2011. An analytical method to transform geocentric into geodetic coordinates. Journal of Geodesy, 85(2), pp.105-117. """ lat, lon, height = _cartesian_to_geodetic(x.to('m').value, y.to('m').value, z.to('m').value, ell=ell, degrees=True) return lat * u.deg, lon * u.deg, height * u.m
def pz90_atm_corr(height: u.m) -> u.mGal: """Return PZ-90 atmospheric correction, in mGal. Parameters ---------- height : ~astropy.units.Quantity Height above sea level. Returns ------- ~astropy.units.Quantity Atmospheric correction. """ height = height.to('km').value return 0.87 * np.exp(-0.116 * (height)**(1.047)) * u.mGal
def grs80_atm_corr_interp(height: u.m, kind: str = 'linear') -> u.mGal: """Return GRS 80 atmospheric correction, in mGal. Interpolated from the table data [1]_. Note: If height < 0 m or height > 40000 m, then correction is extrapolated Parameters ---------- height : ~astropy.units.Quantity Height above sea level. kind : str or int, optional Specifies the kind of interpolation as a string ('linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic' where 'zero', 'slinear', 'quadratic' and 'cubic' refer to a spline interpolation of zeroth, first, second or third order) or as an integer specifying the order of the spline interpolator to use. Default is 'linear'. Returns ------- ~astropy.units.Quantity Atmospheric correction. References ---------- .. [1] Moritz, H. (1980). Geodetic reference system 1980. Bulletin Géodésique, 54(3), 395-405 """ fname = os.path.join(os.path.dirname(__file__), 'data/IAG_atmosphere_correction_table.txt') table_heights, corr = np.loadtxt(fname, unpack=True, delimiter=',', skiprows=4, dtype=float) interp = interp1d(table_heights * 1000, corr, kind=kind, fill_value='extrapolate', assume_sorted=True) return interp(height.to('m').value) * u.mGal
def wenzel_atm_corr(height: u.m) -> u.mGal: """Return atmospheric correction by Wenzel, in mGal. Parameters ---------- height : ~astropy.units.Quantity Height above sea level. Returns ------- ~astropy.units.Quantity Atmospheric correction. References ---------- .. [1] Wenzel, H., 1985, Hochauflosende Kugelfunktionsmodelle fur des Gravitationspotential der Erde [1]: Wissenschaftliche arbeiten der Fachrichtung Vermessungswesen der Universitat Hannover, 137 """ height = height.to('m').value return (0.874 - 9.9e-5 * height + 3.56e-9 * height**2) * u.mGal
def semi_circular_loop(length: u.m, latitude: u.deg = 0 * u.deg): """ Return a Heliographic Stonyhurst coordinate object with points of a semi circular loop in it. """ r_sun = constants.radius def r_2_func(x): return np.arccos(0.5 * x / r_sun.to(u.cm).value) - np.pi + length.to( u.cm).value / 2. / x # Find the loop radius corresponding to the loop length r_2 = scipy.optimize.bisect(r_2_func, length.to(u.cm).value / (2 * np.pi), length.to(u.cm).value / np.pi) * u.cm alpha = np.arccos(0.5 * (r_2 / r_sun)) phi = np.linspace(-np.pi * u.rad + alpha, np.pi * u.rad - alpha, 2000) hcc_frame = frames.Heliocentric(observer=frames.HeliographicStonyhurst( lon=0 * u.deg, lat=latitude, radius=1 * u.AU)) return SkyCoord(x=r_2 * np.sin(phi), y=0 * u.cm, z=r_2 * np.cos(phi) + r_sun, frame=hcc_frame).transform_to('heliographic_stonyhurst')
def __init__(self, moment: u.A * u.m**2, p0: u.m): self.moment = moment.to(u.A * u.m * u.m).value self.p0 = p0.to(u.m).value
def __init__(self, direction, p0: u.m, current: u.A): self.direction = direction / np.linalg.norm(direction) self.p0 = p0.to(u.m).value self.current = current.to(u.A).value
def __init__(self, p1: u.m, p2: u.m, current: u.A): self.p1 = p1.to(u.m).value self.p2 = p2.to(u.m).value if np.all(p1 == p2): raise ValueError("p1, p2 should not be the same point.") self.current = current.to(u.A).value
def __init__( self, # scalars pixel_diameter: u.m, pixel_fill_factor, focal_length: u.m / u.radian, mirror_area: u.m**2, # arrays (vs wavelength) telescope_transmissivity, mirror_reflectivity, window_transmissivity, pde, ): """ Calculate parameters related to the camera efficiency Formulae and data obtained from the excel files at: https://www.mpi-hd.mpg.de/hfm/CTA/MC/Prod4/Config/Efficiencies Credit: Konrad Bernloehr """ self.wavelength = np.arange(200, 1000) << u.nm size = self.wavelength.size if not telescope_transmissivity.size == size: raise ValueError( "All arrays must specify values over full wavelength range") if not mirror_reflectivity.size == size: raise ValueError( "All arrays must specify values over full wavelength range") if not window_transmissivity.size == size: raise ValueError( "All arrays must specify values over full wavelength range") if not pde.size == size: raise ValueError( "All arrays must specify values over full wavelength range") self.pixel_diameter = pixel_diameter.to('m') self.pixel_fill_factor = pixel_fill_factor self.focal_length = focal_length.to("m/radian") self.mirror_area = mirror_area.to("m2") self.telescope_transmissivity = telescope_transmissivity self.mirror_reflectivity = mirror_reflectivity self.window_transmissivity = window_transmissivity self._pde = pde self._pde_scale = 1 self._cherenkov_scale = 1 # Read environment arrays df_env = pd.read_csv(PATH_ENV) diff_flux_unit = u.Unit('10^9 / (nm s m^2 sr)') self._nsb_diff_flux = df_env['nsb_site'].values << diff_flux_unit self._moonlight_diff_flux = df_env['moonlight'].values << diff_flux_unit self._atmospheric_transmissivity = df_env[ 'atmospheric_transmissivity'].values << 1 / u.nm # Scale cherenkov spectrum to match normalisation # scaled to 100 photons/m2 in the wavelength range from 300–600 nm # a value typical for γ-ray showers of about 500 GeV viewed at small core distances # (from https://jama.cta-observatory.org/perspective.req#/items/28666) # TODO: check wrt area cherenkov_integral = self._integrate_cherenkov( self._cherenkov_diff_flux_on_ground, u.Quantity(300, u.nm), u.Quantity(600, u.nm)) self._cherenkov_scale = 100 / cherenkov_integral self._nsb_flux_300_650 = self._integrate_nsb( self._nsb_diff_flux_on_ground, u.Quantity(300, u.nm), u.Quantity(650, u.nm)) self._moonlight_flux_300_650 = self._integrate_nsb( self._moonlight_diff_flux_on_ground, u.Quantity(300, u.nm), u.Quantity(650, u.nm))