def __init__(self, dec: Quantity, equinox: float, plane_id: int, ra: Quantity): try: dec.to(u.degree) except u.UnitConversionError: raise ValueError("The declination must have an angular unit.") if dec.to_value(u.degree) < -90 or dec.to_value(u.degree) > 90: raise ValueError( "The declination must be between -90 and 90 degrees.") if 199.9 < equinox < 200.1: equinox = 2000 if equinox < 1900: raise ValueError("The equinox must be 1900 or later.") try: ra.to(u.degree) except u.UnitConversionError: raise ValueError("The right ascension must have an angular unit.") if ra.to_value(u.degree) < 0 or ra.to_value(u.degree) >= 360: raise ValueError( "The right ascension must have a value between 0 degress " "(inclusive) and 360 degrees (exclusive).") self._dec = dec self._equinox = equinox self._plane_id = plane_id self._ra = ra
def make_bounds( origin: coord.UnitSphericalRepresentation, rot_lower: u.Quantity = _make_bounds_defaults["rot_lower"], rot_upper: u.Quantity = _make_bounds_defaults["rot_upper"], origin_lim: u.Quantity = _make_bounds_defaults["origin_lim"], ) -> T.Tuple[float, float]: """Make bounds on Rotation parameter. Parameters ---------- rot_lower, rot_upper : |Quantity|, optional The lower and upper bounds in degrees. origin_lim : |Quantity|, optional The symmetric lower and upper bounds on origin in degrees. Returns ------- bounds : ndarray Shape (3, 2) Rows are rotation_bounds, lon_bounds, lat_bounds """ rotation_bounds = (rot_lower.to_value(u.deg), rot_upper.to_value(u.deg)) # longitude bounds (ra in ICRS). lon_bounds = (origin.lon + (-1, 1) * origin_lim).to_value(u.deg) # latitude bounds (dec in ICRS). lat_bounds = (origin.lat + (-1, 1) * origin_lim).to_value(u.deg) # stack bounds so rows are bounds. bounds = np.c_[rotation_bounds, lon_bounds, lat_bounds].T return bounds
def test_conversion(self): q_pv = Quantity(self.pv, self.pv_unit) q1 = q_pv.to(('AU', 'AU/day')) assert isinstance(q1, Quantity) assert q1['p'].unit == u.AU assert q1['v'].unit == u.AU / u.day assert np.all(q1['p'] == q_pv['p'].to(u.AU)) assert np.all(q1['v'] == q_pv['v'].to(u.AU/u.day)) q2 = q_pv.to(self.pv_unit) assert q2['p'].unit == self.p_unit assert q2['v'].unit == self.v_unit assert np.all(q2['p'].value == self.pv['p']) assert np.all(q2['v'].value == self.pv['v']) assert not np.may_share_memory(q2, q_pv) pv1 = q_pv.to_value(('AU', 'AU/day')) assert type(pv1) is np.ndarray assert np.all(pv1['p'] == q_pv['p'].to_value(u.AU)) assert np.all(pv1['v'] == q_pv['v'].to_value(u.AU/u.day)) pv11 = q_pv[1].to_value(('AU', 'AU/day')) assert type(pv11) is np.void assert pv11 == pv1[1] q_pv_t = Quantity(self.pv_t, self.pv_t_unit) q2 = q_pv_t.to((('kpc', 'kpc/Myr'), 'Myr')) assert q2['pv']['p'].unit == u.kpc assert q2['pv']['v'].unit == u.kpc / u.Myr assert q2['t'].unit == u.Myr assert np.all(q2['pv']['p'] == q_pv_t['pv']['p'].to(u.kpc)) assert np.all(q2['pv']['v'] == q_pv_t['pv']['v'].to(u.kpc/u.Myr)) assert np.all(q2['t'] == q_pv_t['t'].to(u.Myr))
def __init__( self, content_checksum: str, content_length: Quantity, identifier: uuid.UUID, name: str, plane_id: int, paths: CalibrationLevelPaths, product_type: ProductType, ): if len(content_checksum) > 32: raise ValueError( "The content checksum must have at most 32 characters.") try: content_length.to(byte) except u.UnitConversionError: raise ValueError("The content length must have a file size unit.") if content_length.to_value(byte) <= 0: raise ValueError("The content length must be positive.") if len(name) > 200: raise ValueError( "The artifact name must have at most 200 characters.") if paths.raw is None and paths.reduced is None: raise ValueError("At least one of the paths must be non-None.") self._content_checksum = content_checksum self._content_length = content_length self._identifier = identifier self._name = name self._paths = paths self._plane_id = plane_id self._product_type = product_type
def _prepare_samples(self, frequency: u.Quantity) -> np.ndarray: """Interpolate aperture plane to selected frequencies, and optionally rotate.""" frequency_Hz = frequency.to_value(u.Hz).astype(np.float32, copy=False, casting='same_kind') samples = self._interp_samples(frequency_Hz) return samples
def rss_resolution_element(grating_frequency: Quantity, grating_angle: Quantity, slit_width: Quantity) -> Quantity: """ Returns the resolution element for the given grating frequency, grating angle and slit width. Parameters ---------- grating_frequency: The grating frequency grating_angle: The grating angle slit_width: The slit width Return ------ resolution_element The resolution element """ Lambda = 1 / grating_frequency # TODO some thing is not right below units were supposed to be arcsec but got arcsec * mm return (slit_width.to_value(u.arcsec) * Lambda * np.cos(grating_angle) * (FOCAL_LENGTH_TELESCOPE / FOCAL_LENGTH_RSS_COLLIMATOR))
def column_to_values(self, colname, unit): # First make sure the column is a quantity quantity = Quantity(self.time_series[colname], copy=False) if quantity.unit.is_equivalent(unit): return quantity.to_value(unit) else: raise UnitsError(f"Cannot convert the units '{quantity.unit}' of " f"column '{colname}' to the required units of " f"'{unit}'")
def __init__(self, step: float, frequency: u.Quantity, samples: np.ndarray, *, band: str) -> None: if len(samples) != len(frequency): raise ValueError('frequency and samples have inconsistent shape') self._step = step self._samples = samples self._frequency = frequency self._band = band self._interp_samples = scipy.interpolate.interp1d(frequency.to_value( u.Hz), samples, axis=0, copy=False, bounds_error=False, fill_value=np.nan, assume_sorted=True)
def evaluate(self, method=None, **kwargs): """Evaluate NDData Array This function provides a uniform interface to several interpolators. The evaluation nodes are given as ``kwargs``. Currently available: `~scipy.interpolate.RegularGridInterpolator`, methods: linear, nearest Parameters ---------- method : str {'linear', 'nearest'}, optional Interpolation method kwargs : dict Keys are the axis names, Values the evaluation points Returns ------- array : `~astropy.units.Quantity` Interpolated values, axis order is the same as for the NDData array """ values = [] for idx, axis in enumerate(self.axes): # Extract values for each axis, default: nodes shape = np.ones(len(self.axes), dtype=int) shape[idx] = -1 default = axis.nodes.reshape(shape) temp = Quantity(kwargs.pop(axis.name, default)) # Transform to correct unit temp = temp.to_value(axis.unit) # Transform to match interpolation behaviour of axis values.append(np.atleast_1d(axis._interp_values(temp))) # This is to catch e.g. typos in axis names if kwargs != {}: raise ValueError("Input given for unknown axis: {}".format(kwargs)) if self._regular_grid_interp is None: self._add_regular_grid_interp() return self._regular_grid_interp(values, method=method, **kwargs)
def sample(self, l: ArrayLike, m: ArrayLike, frequency: u.Quantity, frame: Union[AltAzFrame, RADecFrame], output_type: OutputType, *, out: Optional[np.ndarray] = None) -> np.ndarray: l_ = np.asarray(l) m_ = np.asarray(m) if output_type != OutputType.UNPOLARIZED_POWER: raise NotImplementedError( 'Only UNPOLARIZED_POWER is currently implemented') in_shape = np.broadcast_shapes(l_.shape, m_.shape, frame.shape) out_shape = frequency.shape + in_shape if out is not None: if out.shape != out_shape: raise ValueError( f'out must have shape {out_shape}, not {out.shape}') if out.dtype != np.float32: raise TypeError( f'out must have dtype float32, not {out.dtype}') if not out.flags.c_contiguous: raise ValueError('out must be C contiguous') else: out = np.empty(out_shape, np.float32) frequency_Hz = frequency.to_value(u.Hz).astype(np.float32, copy=False, casting='same_kind') samples = self._interp_samples(frequency_Hz) # Create view with l/m axis flattened to 1D for benefit of numba l_view = np.broadcast_to(l_, in_shape).ravel() m_view = np.broadcast_to(m_, in_shape).ravel() out_view = out.view() out_view.shape = frequency.shape + l_view.shape _sample_impl(l_view, m_view, samples, self._step, out_view) return out
def format_duration(duration: u.Quantity) -> str: seconds = int(round(duration.to_value(u.s))) return '{}:{:02}:{:02}'.format(seconds // 3600, seconds // 60 % 60, seconds % 60)
class BinnedDataAxis(DataAxis): """Data axis for binned data Parameters ---------- lo : `~astropy.units.Quantity` Lower bin edges hi : `~astropy.units.Quantity` Upper bin edges name : str, optional Axis name, default: 'Default' interpolation_mode : str {'linear', 'log'} Interpolation behaviour, default: 'linear' """ def __init__(self, lo, hi, **kwargs): self.lo = Quantity(lo) self.hi = Quantity(hi) if ((self.lo < 0).any() or (self.hi < 0).any()) and kwargs.get("interpolation_mode") == "log": raise ValueError( "Interpolation scaling 'log' only support for positive node values." ) super().__init__(None, **kwargs) @classmethod def logspace(cls, emin, emax, nbins, unit=None, **kwargs): # TODO: splitout log space into a helper function vals = DataAxis.logspace(emin, emax, nbins + 1, unit)._data return cls(vals[:-1], vals[1:], **kwargs) def __str__(self): ss = super().__str__() ss += "\nLower bounds {}".format(self.lo) ss += "\nUpper bounds {}".format(self.hi) return ss @property def bins(self): """Bin edges""" unit = self.lo.unit val = np.append(self.lo.value, self.hi.to_value(unit)[-1]) return val * unit @property def bin_width(self): """Bin width""" return self.hi - self.lo @property def nodes(self): """Evaluation nodes. Depending on the interpolation mode, either log or lin center are returned """ if self.interpolation_mode == "log": return self.log_center() else: return self.lin_center() def lin_center(self): """Linear bin centers""" return (self.lo + self.hi) / 2 def log_center(self): """Logarithmic bin centers""" return np.sqrt(self.lo * self.hi)
def __init__( self, object_id: str, start_time: datetime, end_time: datetime, stepsize: Quantity, ): SALT_OBSERVATORY_ID = "B31" # enforce timezones if start_time.tzinfo is None or end_time.tzinfo is None: raise ValueError("The start and end time must be timezone-aware.") # avoid overly excessive queries self.stepsize = stepsize if self.stepsize < 5 * u.minute: raise ValueError( "The sampling interval must be at least 5 minutes.") # query Horizons self.object_id = object_id start = start_time.astimezone(tzutc()).strftime("%Y-%m-%d %H:%M:%S") # Make sure the whole time interval is covered by the queried ephemerides end_time_with_margin = end_time + timedelta( seconds=stepsize.to_value(u.second)) stop = end_time_with_margin.astimezone( tzutc()).strftime("%Y-%m-%d %H:%M:%S") # Horizons requires an int for the step size. As round() might call NumPy's # round method and thus produce a float, we have to round "manually" using # the int function. step = f"{int(0.5 + stepsize.to_value(u.minute))}m" obj = Horizons( id=self.object_id, location=SALT_OBSERVATORY_ID, epochs={ "start": start, "stop": stop, "step": step }, ) ephemerides = obj.ephemerides() # store the ephemerides in the format we need self._ephemerides = [] for row in range(len(ephemerides)): epoch = parse( ephemerides["datetime_str"][row]).replace(tzinfo=tzutc()) ra = float(ephemerides["RA"][row]) * u.deg dec = float(ephemerides["DEC"][row]) * u.deg ra_rate = float(ephemerides["RA_rate"][row]) * u.arcsec / u.hour dec_rate = ephemerides["DEC_rate"][row] * u.arcsec / u.hour magnitude = ephemerides["V"][row] if "V" in ephemerides.keys( ) else 0 magnitude_range = MagnitudeRange(min_magnitude=magnitude, max_magnitude=magnitude, bandpass="******") self._ephemerides.append( Ephemeris( ra=ra, dec=dec, ra_rate=ra_rate, dec_rate=dec_rate, magnitude_range=magnitude_range, epoch=epoch, ))
def image(self, ra: Quantity, dec: Quantity) -> pyfits.HDUList: # grab 10' x 10' image from server and pull it into pyfits if self.survey in SurveyImageService.STSCI_SURVEYS: survey_identifiers = { Survey.POSS2UKSTU_RED: "poss2ukstu_red", Survey.POSS2UKSTU_BLUE: "poss2ukstu_blue", Survey.POSS2UKSTU_IR: "poss2ukstu_ir", Survey.POSS1_RED: "poss1_red", Survey.POSS1_BLUE: "poss1_blue", } url = "https://archive.stsci.edu/cgi-bin/dss_search" params = urllib.parse.urlencode({ "v": survey_identifiers[self.survey], "r": "%f" % ra.to_value(u.deg), "d": "%f" % dec.to_value(u.deg), "e": "J2000", "h": 10.0, "w": 10.0, "f": "fits", "c": "none", }).encode("utf-8") elif self.survey in SurveyImageService.SKY_VIEW_SURVEYS: survey_identifiers = { Survey.TWO_MASS_J: "2mass-j", Survey.TWO_MASS_H: "2mass-h", Survey.TWO_MASS_K: "2mass-k", } ra = Angle(ra) dec = Angle(dec) url = "https://skyview.gsfc.nasa.gov/current/cgi/runquery.pl" params = urllib.parse.urlencode({ "Position": "'%d %d %f, %d %d %f'" % ( round(ra.hms[0]), ra.hms[1], ra.hms[2], round(dec.dms[0]), abs(dec.dms[1]), abs(dec.dms[2]), ), "Survey": survey_identifiers[self.survey], "Coordinates": "J2000", "Return": "FITS", "Pixels": 700, "Size": 0.1667, }).encode("utf-8") else: raise Exception(f"Unsupported survey: {self.survey}") fits_data = io.BytesIO() data = urllib.request.urlopen(url, params).read() fits_data.write(data) fits_data.seek(0) return pyfits.open(fits_data)
def deproject(projection: np.ndarray, projection_azimuth: u.Quantity, spectral_order: int, cube_shape: typ.Tuple[int, ...] = None, cube_spatial_offset: typ.Tuple[int, int] = (0, 0), projection_spatial_offset: typ.Tuple[int, int] = (0, 0), x_axis: int = ~2, y_axis: int = ~1, w_axis: int = ~0, rotation_kwargs: typ.Dict = None) -> np.ndarray: if rotation_kwargs is None: rotation_kwargs = { 'reshape': False, 'prefilter': False, 'order': 3, 'mode': 'nearest', } csh = list(projection.shape) # csh = list(cube_shape) csh[w_axis] = cube_shape[w_axis] projection = np.broadcast_to(projection, csh, subok=True) shifted_projection = np.zeros(cube_shape) px, py = cube_spatial_offset qx, qy = projection_spatial_offset out_sl = [slice(None)] * shifted_projection.ndim in_sl = [slice(None)] * projection.ndim tx = px - qx ty = py - qy out_sl[x_axis] = slice( max(0, tx), min(shifted_projection.shape[x_axis], tx + csh[x_axis])) out_sl[y_axis] = slice( max(0, ty), min(shifted_projection.shape[y_axis], ty + csh[y_axis])) in_sl[x_axis] = slice( max(0, -tx), min(csh[x_axis], -tx + shifted_projection.shape[x_axis])) in_sl[y_axis] = slice( max(0, -ty), min(csh[y_axis], -ty + shifted_projection.shape[y_axis])) shifted_projection[tuple(out_sl)] = projection[in_sl] backprojected_cube = np.zeros_like(shifted_projection) ssh = list(cube_shape) x, y, l = np.meshgrid(np.arange(ssh[x_axis]), np.arange(ssh[y_axis]), np.arange(ssh[w_axis]), copy=False, sparse=False) x, y, l = x.flatten(), y.flatten(), l.flatten() out_sl = [slice(None)] * shifted_projection.ndim in_sl = [slice(None)] * shifted_projection.ndim out_sl[x_axis], out_sl[y_axis], out_sl[w_axis] = x - spectral_order * ( l), y, l in_sl[x_axis], in_sl[y_axis], in_sl[w_axis] = x, y, l backprojected_cube[tuple(out_sl)] = shifted_projection[in_sl] del shifted_projection del x, y, l az = -1 * projection_azimuth.to_value(u.deg) # backprojected_cube = scipy.ndimage.rotate( # input=backprojected_cube, # angle=az, # axes=(x_axis, y_axis), # **rotation_kwargs # ) backprojected_cube = rotate( image=backprojected_cube, angle=az, ) return backprojected_cube
def model(cube: np.ndarray, projection_azimuth: u.Quantity, spectral_order: int, projection_shape: typ.Tuple[int, ...] = None, cube_spatial_offset: typ.Tuple[int, int] = (0, 0), projection_spatial_offset: typ.Tuple[int, int] = (0, 0), x_axis: int = ~2, y_axis: int = ~1, w_axis: int = ~0, rotation_kwargs: typ.Dict = None) -> np.ndarray: """ Model is a basic forward model of CT imaging spectrograph. :param cube: 'slabs' which is a `np.ndarray` which have at least an x, y, and wavelength axis. :param projection_azimuth: scalar angle describing dispersion direction in the `data` array. :param spectral_order: number of indices the array shifted per wavelength bin. :param projection_shape: Desired shape of output array, as tuple of integers :param projection_spatial_offset: :param cube_spatial_offset: :param x_axis: axis of the data slabs representing x-axis :param y_axis: axis of the data slabs representing y-axis :param w_axis: axis of the data slabs representing wavelength axis :param rotation_kwargs: kwargs for `scipy.ndimage.rotate` to be used during rotation portion of forward model :return: list of arrays to which the forward model has been applied """ if rotation_kwargs is None: rotation_kwargs = { 'reshape': False, 'prefilter': False, 'order': 3, 'mode': 'nearest', } cube = cube.copy() az = projection_azimuth.to_value(u.deg) tsh = min_projection_shape(cube, spectral_order, x_axis, w_axis) if projection_shape is None: projection_shape = tsh p_data = np.zeros(projection_shape) px, py = projection_spatial_offset qx, qy = cube_spatial_offset ssh = list(cube.shape) x, y, l = np.meshgrid(np.arange(ssh[x_axis]), np.arange(ssh[y_axis]), np.arange(ssh[w_axis])) x, y, l = x.flatten(), y.flatten(), l.flatten() # rotated_cube = scipy.ndimage.rotate( # input=cube.copy(), # angle=az, # axes=(x_axis, y_axis), # **rotation_kwargs # ) rotated_cube = rotate( image=cube.copy(), angle=az, ) ssh[x_axis] += np.abs(spectral_order) * ssh[w_axis] shifted_cube = np.zeros(ssh) out_sl = [slice(None)] * shifted_cube.ndim in_sl = [slice(None)] * rotated_cube.ndim out_sl[x_axis], out_sl[y_axis], out_sl[ w_axis] = x + spectral_order * l, y, l in_sl[x_axis], in_sl[y_axis], in_sl[w_axis] = x, y, l # Making a little bit future proof: out_sl = tuple(out_sl) in_sl = tuple(in_sl) shifted_cube[out_sl] = rotated_cube[in_sl] out_sl = [slice(None)] * p_data.ndim in_sl = [slice(None)] * shifted_cube.ndim tx = px - qx ty = py - qy out_sl[x_axis] = slice(max(0, tx), min(p_data.shape[x_axis], tx + ssh[x_axis])) out_sl[y_axis] = slice(max(0, ty), min(p_data.shape[y_axis], ty + ssh[y_axis])) in_sl[x_axis] = slice(max(0, -tx), min(ssh[x_axis], -tx + p_data.shape[x_axis])) in_sl[y_axis] = slice(max(0, -ty), min(ssh[y_axis], -ty + p_data.shape[y_axis])) # Making a little bit future proof: # Using the tuple loophole out_sl = tuple(out_sl) in_sl = tuple(in_sl) p_data[out_sl] = np.sum(shifted_cube, axis=w_axis, keepdims=True)[in_sl] return p_data
def smooth(self, width, kernel="gauss"): """Smooth the map. Iterates over 2D image planes, processing one at a time. Parameters ---------- width : `~astropy.units.Quantity`, str or float Smoothing width given as quantity or float. If a float is given it interpreted as smoothing width in pixels. If an (angular) quantity is given it converted to pixels using ``healpy.nside2resol``. It corresponds to the standard deviation in case of a Gaussian kernel, and the radius in case of a disk kernel. kernel : {'gauss', 'disk'} Kernel shape Returns ------- image : `HpxNDMap` Smoothed image (a copy, the original object is unchanged). """ import healpy as hp if not self.geom.is_allsky: raise NotImplementedError( "Smoothing is only possible for all-sky maps") nside = self.geom.nside lmax = 3 * nside - 1 # maximum l of the power spectrum nest = self.geom.nest # The smoothing width is expected by healpy in radians if isinstance(width, (Quantity, str)): width = Quantity(width) width = width.to_value("rad") else: binsz = np.degrees(hp.nside2resol(nside)) width = width * binsz width = np.deg2rad(width) smoothed_data = np.empty(self.data.shape, dtype=float) for img, idx in self.iter_by_image(): img = img.astype(float) if nest: # reorder to ring to do the smoothing img = hp.pixelfunc.reorder(img, n2r=True) if kernel == "gauss": data = hp.sphtfunc.smoothing(img, sigma=width, pol=False, verbose=False, lmax=lmax) elif kernel == "disk": # create the step function in angular space theta = np.linspace(0, width) beam = np.ones(len(theta)) beam[theta > width] = 0 # convert to the spherical harmonics space window_beam = hp.sphtfunc.beam2bl(beam, theta, lmax) # normalize the window beam window_beam = window_beam / window_beam.max() data = hp.sphtfunc.smoothing(img, beam_window=window_beam, pol=False, verbose=False, lmax=lmax) else: raise ValueError(f"Invalid kernel: {kernel!r}") if nest: # reorder back to nest after the smoothing data = hp.pixelfunc.reorder(data, r2n=True) smoothed_data[idx] = data return self._init_copy(data=smoothed_data)