def test_add_row_cannot_mask_column_raises_typeerror(self): t = QTable() t['a'] = [1, 2] * u.m t.add_row((3 * u.m,)) # No problem with pytest.raises(ValueError) as exc: t.add_row((3 * u.m,), mask=(True,)) assert (exc.value.args[0].splitlines() == ["Unable to insert row because of exception in column 'a':", "mask was supplied for column 'a' but it does not support masked values"])
def line_stats(self, x): """ Calculate statistics over individual line profiles. Parameters ---------- x : :class:`~u.Quantity` The input dispersion in either wavelength/frequency or velocity space. Returns ------- tab : :class:`~astropy.table.QTable` A table detailing the calculated statistics. """ tab = QTable(names=[ 'name', 'wave', 'col_dens', 'v_dop', 'delta_v', 'delta_lambda', 'ew', 'dv90', 'fwhm' ], dtype=('S10', 'f8', 'f8', 'f8', 'f8', 'f8', 'f8', 'f8', 'f8')) tab['wave'].unit = u.AA tab['v_dop'].unit = u.km / u.s tab['ew'].unit = u.AA tab['dv90'].unit = u.km / u.s tab['fwhm'].unit = u.AA tab['delta_v'].unit = u.km / u.s tab['delta_lambda'].unit = u.AA for line in self.lines: disp_equiv = u.spectral() + DOPPLER_CONVERT[ self.velocity_convention](line.lambda_0.quantity) with u.set_enabled_equivalencies(disp_equiv): vel = x.to('km/s') wav = x.to('Angstrom') # Generate the spectrum1d object for this line profile ew = equivalent_width(wav, line(wav)) dv90 = delta_v_90(vel, line(wav)) fwhm = full_width_half_max(wav, line(wav)) tab.add_row([ line.name, line.lambda_0, line.column_density, line.v_doppler, line.delta_v, line.delta_lambda, ew, dv90, fwhm ]) return tab
def fits_1(): """ COSMOS2015v1.1: length = 1182108 pdz_cosmos2015_v1.3_t1: length = 606887 FLAG_COSMOS: length = 576762 FLAG_HJMCC: length = 606887 FLAG_DEEP: length = 227278 FLAG_PETER: length = 539706 """ data_full = fits.open('COSMOS2015v1.1.fits')[1].data data_phot = Table.read('pdz_cosmos2015_v1.3_t1.csv', format='ascii.csv') # print(data['FLAG_HJMCC']) # print(data['FLAG_DEEP']) # print(data['FLAG_COSMOS']) # print(data['FLAG_PETER']) # print(data['number']) ind_set = set() ind_dict = {} for i, ind in enumerate(data_phot['ID']): ind_set.add(ind) ind_dict[ind] = i max_ind = 1182108 subset = 'FLAG_PETER' names = data_phot.colnames values = [[] for i in names] table = QTable(values, names=data_phot.colnames) for i in range(max_ind): if data_full[subset][i] == 0 and (i + 1) in ind_set: ind = ind_dict[i + 1] print(str(i + 1)[:2], end=' ') table.add_row(data_phot[ind]) ascii.write(table, f'{subset}.csv', format='csv')
def region_stats(self, x, rest_wavelength, rel_tol=1e-2, abs_tol=1e-5): """ Calculate statistics over arbitrary line regions given some tolerance from the continuum. Parameters ---------- x : :class:`~u.Quantity` The input dispersion in either wavelength/frequency or velocity space. rest_wavelength : :class:`~u.Quantity` The rest frame wavelength used in conversions between wavelength/ frequency and velocity space. rel_tol : float The relative tolerance parameter. abs_tol : float The absolute tolerance parameter. Returns ------- tab : :class:`~astropy.table.QTable` A table detailing the calculated statistics. """ y = self(x) if self.output_type == 'flux': y = self.continuum(x) - y else: y -= self.continuum(x) # Calculate the regions in the raw data # absolute(a - b) <= (atol + rtol * absolute(b)) regions = {(reg[0], reg[1]): [] for reg in find_regions(y, rel_tol=rel_tol, abs_tol=abs_tol) } tab = QTable(names=[ 'region_start', 'region_end', 'rest_wavelength', 'ew', 'dv90', 'fwhm' ], dtype=('f8', 'f8', 'f8', 'f8', 'f8', 'f8')) tab['region_start'].unit = x.unit tab['region_end'].unit = x.unit tab['rest_wavelength'].unit = u.AA tab['ew'].unit = u.AA tab['dv90'].unit = u.km / u.s tab['fwhm'].unit = u.AA for mn_bnd, mx_bnd in regions: mask = (x > x[mn_bnd]) & (x < x[mx_bnd]) x_reg = x[mask] y_reg = y[mask] disp_equiv = u.spectral() + DOPPLER_CONVERT[ self.velocity_convention](rest_wavelength) with u.set_enabled_equivalencies(disp_equiv): vel = x_reg.to('km/s') wav = x_reg.to('Angstrom') # Generate the spectrum1d object for this line profile ew = equivalent_width(wav, y_reg) dv90 = delta_v_90(vel, y_reg) fwhm = full_width_half_max(wav, y_reg) tab.add_row( [x[mn_bnd], x[mx_bnd], rest_wavelength, ew, dv90, fwhm]) return tab
def group_table(self, edges): """Compute bin groups table for the map axis, given coarser bin edges. Parameters ---------- edges : `~astropy.units.Quantity` Group bin edges. Returns ------- groups : `~astropy.table.Table` Map axis group table. """ # TODO: try to simplify this code if not self.node_type == "edges": raise ValueError("Only edge based map axis can be grouped") edges_pix = self.coord_to_pix(edges) edges_pix = np.clip(edges_pix, -0.5, self.nbin - 0.5) edges_idx = np.round(edges_pix + 0.5) - 0.5 edges_idx = np.unique(edges_idx) edges_ref = self.pix_to_coord(edges_idx) * self.unit groups = QTable() groups["{}_min".format(self.name)] = edges_ref[:-1] groups["{}_max".format(self.name)] = edges_ref[1:] groups["idx_min"] = (edges_idx[:-1] + 0.5).astype(int) groups["idx_max"] = (edges_idx[1:] - 0.5).astype(int) if len(groups) == 0: raise ValueError("No overlap between reference and target edges.") groups["bin_type"] = "normal " edge_idx_start, edge_ref_start = edges_idx[0], edges_ref[0] if edge_idx_start > 0: underflow = { "bin_type": "underflow", "idx_min": 0, "idx_max": edge_idx_start, "{}_min".format(self.name): self.pix_to_coord(-0.5) * self.unit, "{}_max".format(self.name): edge_ref_start, } groups.insert_row(0, vals=underflow) edge_idx_end, edge_ref_end = edges_idx[-1], edges_ref[-1] if edge_idx_end < (self.nbin - 0.5): overflow = { "bin_type": "overflow", "idx_min": edge_idx_end + 1, "idx_max": self.nbin - 1, "{}_min".format(self.name): edge_ref_end, "{}_max".format(self.name): self.pix_to_coord(self.nbin - 0.5) * self.unit, } groups.add_row(vals=overflow) group_idx = Column(np.arange(len(groups))) groups.add_column(group_idx, name="group_idx", index=0) return groups
def apply(self, data, name, unit=None): """Apply an arbitrarily shaped sequence as additional column to a `~sbpy.data.DataClass` object and reshape it accordingly. Parameters ---------- data : list or iterable `~astropy.units.Quantity` object Data to be added in a new column in form of a one-dimensional list or a two-dimensional nested sequence. Each element in ``data`` corresponds to one of the rows in the existing data table. If an element of ``data`` is a list, the corresponding data table row is repeated the same the number of times as there are elements in this sublist. If ``data`` is provided as a flat list and has the same length as the current data table, ``data`` will be simply added as a column to the data table and the length of the data table will not change. If ``data`` is provided as a `~astropy.units.Quantity` object (only possible for flat lists), its unit is adopted, unless ``unit`` is specified (not None). name : str Name of the new data column. unit : `~astropy.units` object or str, optional Unit to be applied to the new column. Default: `None` Returns ------- None Note ---- As a result of this method, the length of the underlying data table will be the same as the length of the flattened `data` parameter. Examples -------- Imagine the following scenario: you obtain photometric measurements of the same asteroid over a number of nights. The following `~sbpy.data.Ephem` object summarizes the observations: >>> from sbpy.data import Ephem >>> import astropy.units as u >>> obs = Ephem.from_columns([[2451223, 2451224, 2451226]*u.d, ... [120.1, 121.3, 124.9]*u.deg, ... [12.4, 12.2, 10.8]*u.deg], ... names=('JD', 'RA', 'DEC')) >>> obs <QTable length=3> JD RA DEC d deg deg float64 float64 float64 --------- ------- ------- 2451223.0 120.1 12.4 2451224.0 121.3 12.2 2451226.0 124.9 10.8 After analyzing the observations, you would like to add the measured apparent V-band magnitudes to this object. You have one observation from the first night, two from the second night, and three from the third night. Instead of re-creating ``obs``, `~sbpy.data.DataClass.apply` offers a convenient way to supplement ``obs``: >>> obs.apply([[12.1], [12.5, 12.6], [13.5, 13.4, 13.5]], ... name='V', unit='mag') >>> obs <QTable length=6> JD RA DEC V d deg deg mag float64 float64 float64 float64 --------- ------- ------- ------- 2451223.0 120.1 12.4 12.1 2451224.0 121.3 12.2 12.5 2451224.0 121.3 12.2 12.6 2451226.0 124.9 10.8 13.5 2451226.0 124.9 10.8 13.4 2451226.0 124.9 10.8 13.5 Note how the data table has been re-arranged and rows have been duplicated in order to provide the expected shape. """ _newtable = None # strip units off Quantity objects if isinstance(data, u.Quantity): unit = data.unit data = data.value if len(data) != len(self.table): raise DataClassError('Data parameter must have ' 'same length as self._table') _newcolumn = array([]) for i, val in enumerate(data): if not isinstance(val, (list, tuple, ndarray)): val = [val] _newcolumn = hstack([_newcolumn, val]) # add corresponding row from _table for each element in val for j in range(len(val)): # initialize new QTable object if _newtable is None: _newtable = QTable(self.table[0]) continue _newtable.add_row(self.table[i]) # add new column _newtable.add_column(Column(_newcolumn, name=name, unit=unit)) self._table = _newtable
class DataClass(): """`~sbpy.data.DataClass` serves as the base class for all data container classes in `sbpy` in order to provide consistent functionality throughout all these classes. The core of `~sbpy.data.DataClass` is an `~astropy.table.QTable` object (referred to as the `data table` below) - a type of `~astropy.table.Table` object that supports the `~astropy.units` formalism on a per-column base - which already provides most of the required functionality. `~sbpy.data.DataClass` objects can be manually generated from dictionaries (`~sbpy.data.DataClass.from_dict`), `~numpy.array`-like (`~sbpy.data.DataClass.from_array`) objects, or directly from another `~astropy.table.QTable` object. A few high-level functions for table data access or modification are provided; other, more complex modifications can be applied to the underlying table object (`~sbpy.data.DataClass.table`) directly. """ def __init__(self, data): self._table = QTable() # self.altkeys = {} # dictionary for alternative column names if (len(data.items()) == 1 and 'table' in data.keys()): # single item provided named 'table' -> already Table object self._table = QTable(data['table']) else: # treat kwargs as dictionary for key, val in data.items(): try: unit = val.unit val = val.value except AttributeError: unit = None # check if val is already list-like try: val[0] except (TypeError, IndexError): val = [val] self._table[key] = Column(val, unit=unit) @classmethod def from_dict(cls, data): """Create `~sbpy.data.DataClass` object from dictionary or list of dictionaries. Parameters ---------- data : `~collections.OrderedDict`, dictionary or list (or similar) of dictionaries Data that will be ingested in `~sbpy.data.DataClass` object. Each dictionary creates a row in the data table. Dictionary keys are used as column names; corresponding values must be scalar (cannot be lists or arrays). If a list of dictionaries is provided, all dictionaries have to provide the same set of keys (and units, if used at all). Returns ------- `DataClass` object Examples -------- >>> import astropy.units as u >>> from sbpy.data import Orbit >>> orb = Orbit.from_dict({'a': 2.7674*u.au, ... 'e': 0.0756, ... 'i': 10.59321*u.deg}) Since dictionaries have no specific order, the ordering of the column in the example above is not defined. If your data table requires a specific order, use an ``OrderedDict``: >>> from collections import OrderedDict >>> orb = Orbit.from_dict(OrderedDict([('a', 2.7674*u.au), ... ('e', 0.0756), ... ('i', 10.59321*u.deg)])) >>> print(orb) <QTable length=1> a e i AU deg float64 float64 float64 ------- ------- -------- 2.7674 0.0756 10.59321 >>> print(orb.column_names) # doctest: +SKIP <TableColumns names=('a','e','i')> >>> print(orb.table['a', 'e', 'i']) a e i AU deg ------ ------ -------- 2.7674 0.0756 10.59321 """ if isinstance(data, (dict, OrderedDict)): return cls(data) elif isinstance(data, (list, ndarray, tuple)): # build table from first dict and append remaining rows tab = cls(data[0]) for row in data[1:]: tab.add_rows(row) return tab else: raise TypeError('this function requires a dictionary or a ' 'list of dictionaries') @classmethod def from_array(cls, data, names): """Create `~sbpy.data.DataClass` object from list, `~numpy.ndarray`, or tuple. Parameters ---------- data : list, `~numpy.ndarray`, or tuple Data that will be ingested in `DataClass` object. A one dimensional sequence will be interpreted as a single row. Each element that is itself a sequence will be interpreted as a column. names : list Column names, must have the same number of names as data columns. Returns ------- `DataClass` object Examples -------- >>> from sbpy.data import DataClass >>> import astropy.units as u >>> dat = DataClass.from_array([[1, 2, 3]*u.deg, ... [4, 5, 6]*u.km, ... ['a', 'b', 'c']], ... names=('a', 'b', 'c')) >>> print(dat.table) a b c deg km --- --- --- 1.0 4.0 a 2.0 5.0 b 3.0 6.0 c """ if isinstance(data, (list, ndarray, tuple)): return cls.from_dict(OrderedDict(zip(names, data))) else: raise TypeError('this function requires a list, tuple or a ' 'numpy array') @classmethod def from_table(cls, data): """Create `DataClass` object from `~astropy.table.Table` or `astropy.table.QTable` object. Parameters ---------- data : astropy `Table` object, mandatory Data that will be ingested in `DataClass` object. Returns ------- `DataClass` object Examples -------- >>> from astropy.table import QTable >>> import astropy.units as u >>> from sbpy.data import DataClass >>> tab = QTable([[1,2,3]*u.kg, ... [4,5,6]*u.m/u.s,], ... names=['mass', 'velocity']) >>> dat = DataClass.from_table(tab) >>> print(dat.table) mass velocity kg m / s ---- -------- 1.0 4.0 2.0 5.0 3.0 6.0 """ return cls({'table': data}) @classmethod def from_file(cls, filename, **kwargs): """Create `DataClass` object from a file using `~astropy.table.Table.read`. Parameters ---------- filename : str Name of the file that will be read and parsed. **kwargs : additional parameters Optional parameters that will be passed on to `~astropy.table.Table.read`. Returns ------- `DataClass` object Notes ----- This function is merely a wrapper around `~astropy.table.Table.read`. Please refer to the documentation of that function for additional information on optional parameters and data formats that are available. Furthermore, note that this function is not able to identify units. If you want to work with `~astropy.units` you have to assign them manually to the object columns. Examples -------- >>> from sbpy.data import DataClass >>> dat = DataClass.from_file('data.txt', ... format='ascii') # doctest: +SKIP """ data = QTable.read(filename, **kwargs) return cls({'table': data}) def to_file(self, filename, format='ascii', **kwargs): """Write object to a file using `~astropy.table.Table.write`. Parameters ---------- filename : str Name of the file that will be written. format : str, optional Data format in which the file should be written. Default: ``ASCII`` **kwargs : additional parameters Optional parameters that will be passed on to `~astropy.table.Table.write`. Returns ------- None Notes ----- This function is merely a wrapper around `~astropy.table.Table.write`. Please refer to the documentation of that function for additional information on optional parameters and data formats that are available. Furthermore, note that this function is not able to write unit information to the file. Examples -------- >>> from sbpy.data import DataClass >>> import astropy.units as u >>> dat = DataClass.from_array([[1, 2, 3]*u.deg, ... [4, 5, 6]*u.km, ... ['a', 'b', 'c']], ... names=('a', 'b', 'c')) >>> dat.to_file('test.txt') """ self._table.write(filename, format=format, **kwargs) def __len__(self): """Get number of data elements in _table""" return len(self._table) def __getattr__(self, field): """Get attribute from ``self._table` (columns, rows); checks for and may use alternative field names.""" if field in dir(self): return self.field else: try: field = self._translate_columns(field)[0] return self._table[field] except (KeyError, IndexError, AttributeError): raise AttributeError('Attribute {:s} not available.'.format( field)) def __repr__(self): """Return representation of the underlying data table (``self._table.__repr__()``)""" return self._table.__repr__() def __getitem__(self, ident): """Return columns or rows from data table (``self._table``); checks for and may use alternative field names.""" # iterable if isinstance(ident, (list, tuple, ndarray)): if all([isinstance(i, str) for i in ident]): # list of column names self = self._convert_columns(ident) newkeylist = [self._translate_columns(i)[0] for i in ident] ident = newkeylist # return as new DataClass object return self.from_table(self._table[ident]) # ignore lists of boolean (masks) elif all([isinstance(i, bool) for i in ident]): pass # ignore lists of integers elif all([isinstance(i, int) for i in ident]): pass # individual strings elif isinstance(ident, str): self = self._convert_columns(ident) ident = self._translate_columns(ident)[0] # return as element from self_table return self._table[ident] def __setitem__(self, *args): """Refer cls.__setitem__ to self._table""" self._table.__setitem__(*args) def _translate_columns(self, target_colnames): """Translate target_colnames to the corresponding column names present in this object's table. Returns a list of actual column names present in this object that corresponds to target_colnames (order is preserved). Raises KeyError if not all columns are present or one or more columns could not be translated. """ if not isinstance(target_colnames, (list, ndarray, tuple)): target_colnames = [target_colnames] translated_colnames = deepcopy(target_colnames) for idx, colname in enumerate(target_colnames): # colname is already a column name in self.table if colname in self.column_names: continue # colname is an alternative column name elif colname in sum(conf.fieldnames, []): for alt in conf.fieldnames[conf.fieldname_idx[colname]]: # translation available for colname if alt in self.column_names: translated_colnames[idx] = alt break # colname is unknown, raise a KeyError else: raise KeyError('field {:s} not available.'.format( colname)) return translated_colnames def _convert_columns(self, target_colnames): """Convert target_colnames, if necessary. Converted columns will be added as columns to ``self`` using the field names provided in target_colnames. No error is returned by this function if a field could not be converted. """ if not isinstance(target_colnames, (list, ndarray, tuple)): target_colnames = [target_colnames] for colname in target_colnames: # ignore, if colname is unknown (KeyError) try: # ignore if colname has already been converted if any([alt in self.column_names for alt in conf.fieldnames[conf.fieldname_idx[colname]]]): continue # consider alternative names for colname -> alt for alt in conf.fieldnames[conf.fieldname_idx[colname]]: if alt in list(conf.field_eq.keys()): # conversion identified convname = self._translate_columns( list(conf.field_eq[alt].keys())[0])[0] convfunc = list(conf.field_eq[alt].values())[0] if convname in self.column_names: # create new column for the converted field self.add_column(convfunc(self.table[convname]), colname) break except KeyError: continue return self @property def table(self): """Return `~astropy.table.QTable` object containing all data.""" return self._table @property def column_names(self): """Return a list of all column names in the data table.""" return self._table.columns def add_rows(self, rows, join_type='inner'): """Append additional rows to the existing data table. An individual row can be provided in list, tuple, `~numpy.ndarray`, or dictionary form. Multiple rows can be provided in the form of a list, tuple, or `~numpy.ndarray` of individual rows. Multiple rows can also be provided in the form of a `~astropy.table.QTable` or another `~sbpy.data.DataClass` object. Parameter ``join_type`` defines which columns appear in the final output table: ``inner`` only keeps those columns that appear in both the original table and the rows to be added; ``outer`` will keep all columns and populate some with placeholders, if necessary. In case of a list, the list elements must be in the same order as the table columns. In either case, matching `~astropy.units` must be provided in ``rows`` if used in the data table. Parameters ---------- rows : list, tuple, `~numpy.ndarray`, dict, or `~collections.OrderedDict` Data to be appended to the table; required to have the same length as the existing table, as well as the same units. join_type : str, optional Defines which columns are kept in the output table: ``inner`` only keeps those columns that appear in both the original table and the rows to be added; ``outer`` will keep all columns and populate them with placeholders, if necessary. Default: ``inner`` Returns ------- n : int, the total number of rows in the data table Examples -------- >>> from sbpy.data import DataClass >>> import astropy.units as u >>> dat = DataClass.from_array([[1, 2, 3]*u.Unit('m'), ... [4, 5, 6]*u.m/u.s, ... ['a', 'b', 'c']], ... names=('a', 'b', 'c')) >>> dat.add_rows({'a': 5*u.m, 'b': 8*u.m/u.s, 'c': 'e'}) 4 >>> print(dat.table) a b c m m / s --- ----- --- 1.0 4.0 a 2.0 5.0 b 3.0 6.0 c 5.0 8.0 e >>> dat.add_rows(([6*u.m, 9*u.m/u.s, 'f'], ... [7*u.m, 10*u.m/u.s, 'g'])) 6 >>> dat.add_rows(dat) 12 """ if isinstance(rows, QTable): self._table = vstack([self._table, rows], join_type=join_type) if isinstance(rows, DataClass): self._table = vstack([self._table, rows.table], join_type=join_type) if isinstance(rows, (dict, OrderedDict)): try: newrow = [rows[colname] for colname in self._table.columns] except KeyError as e: raise ValueError('data for column {0} missing in row {1}'. format(e, rows)) self.add_rows(newrow) if isinstance(rows, (list, ndarray, tuple)): if (not isinstance(rows[0], (u.quantity.Quantity, float)) and isinstance(rows[0], (dict, OrderedDict, list, ndarray, tuple))): for subrow in rows: self.add_rows(subrow) else: self._table.add_row(rows) return len(self._table) def add_column(self, data, name, **kwargs): """Append a single column to the current data table. The lenght of the input list, `~numpy.ndarray`, or tuple must match the current number of rows in the data table. Parameters ---------- data : list, `~numpy.ndarray`, or tuple Data to be filled into the table; required to have the same length as the existing table's number rows. name : str Name of the new column; must be different from already existing column names. **kwargs : additional parameters Additional optional parameters will be passed on to `~astropy.table.Table.add_column`. Returns ------- n : int, the total number of columns in the data table Examples -------- >>> from sbpy.data import DataClass >>> import astropy.units as u >>> dat = DataClass.from_array([[1, 2, 3]*u.Unit('m'), ... [4, 5, 6]*u.m/u.s, ... ['a', 'b', 'c']], ... names=('a', 'b', 'c')) >>> dat.add_column([10, 20, 30]*u.kg, name='d') 4 >>> print(dat.table) a b c d m m / s kg --- ----- --- ---- 1.0 4.0 a 10.0 2.0 5.0 b 20.0 3.0 6.0 c 30.0 """ self._table.add_column(Column(data, name=name), **kwargs) return len(self.column_names)
def solve_pointing(filename, relax=False): tick = dt.utcnow() log.info(f"Analyzing {filename.name}") hdr = fits.getheader(filename) if not relax: if not hdr.get('PONAME').strip() == 'REF': return None if not float(hdr.get('RAOFF')) < 0.1: return None if not float(hdr.get('DECOFF')) < 0.1: return None if not hdr.get('OBJECT') in ['GuiderFlexureTest', 'MIRA PMFM 350', 'MIRA PMFM -350']: return None # Extract EL, SKYPA, ROTPPOSN, FILTER EL = float(hdr.get('EL')) * u.deg SKYPA1 = float(hdr.get('SKYPA1')) * u.deg SKYPA2 = float(hdr.get('SKYPA2')) * u.deg ROTPPOSN = float(hdr.get('ROTPPOSN')) * u.deg FILTER = hdr.get('FILTER') OBJECT = hdr.get('OBJECT') FCPA, FCEL = (hdr.get('FCPA_EL')).split(' ') # Solve image for astrometry astrometry_cmd = ['solve-field', '-O', '-p', '-z', '2', '-t', '2', f"{f}"] # astrometry_cmd = ['solve-field', '-O', '-p', '-z', '2', '-T', f"{f}"] log.info(' ' + ' '.join(astrometry_cmd[:-1])) output = subprocess.run(astrometry_cmd, stdout=subprocess.PIPE) solved_file = f.with_name(f.name.replace('.fits', '.solved')) new_file = f.with_name(f.name.replace('.fits', '.new')) if not solved_file.exists(): log.error(f' Astrometry solve failed for {filename.name}') return # Open Solved Image hdul = fits.open(new_file) # Extract Guider Pointing Info from Header PONAME = hdul[0].header.get('PONAME') # This should be REF POYPOS = hdul[0].header.get('POYPOS') POXPOS = hdul[0].header.get('POXPOS') POYOFF = hdul[0].header.get('POYOFF') POXOFF = hdul[0].header.get('POXOFF') TARGRA = hdul[0].header.get('TARGRA') TARGDE = hdul[0].header.get('TARGDE') TARGFR = hdul[0].header.get('TARGFR') ROTSTS = hdul[0].header.get('ROTSTS') ROTMOD = hdul[0].header.get('ROTMOD') RAOFF = float(hdul[0].header.get('RAOFF')) * u.arcsec DECOFF = float(hdul[0].header.get('DECOFF')) * u.arcsec RA = float(hdul[0].header.get('RA')) * u.deg DEC = float(hdul[0].header.get('DEC')) * u.deg guider_coord = SkyCoord(RA, DEC, frame='icrs') # Extract WCS from header center_coord = get_center_coord(hdul[0]) # w = WCS(hdul[0].header) # nx, ny = hdul[0].data.shape # result = w.all_pix2world(np.array(nx/2), np.array(ny/2), 1) # center_coord = SkyCoord(result[0], result[1], frame='icrs', unit='deg') offset_pa = center_coord.position_angle(guider_coord).to(u.deg) offset_distance = center_coord.separation(guider_coord).to(u.arcsec) offset_angle = offset_pa.to(u.deg) - SKYPA2 offset_angle.wrap_at(180*u.deg, inplace=True) tock = dt.utcnow() analysis_time = (tock-tick).total_seconds() log.info(f" Solved {analysis_time:.0f} s: {offset_distance:.1f}, "\ f"{offset_angle:.2f} at drive = {ROTPPOSN:.2f}") table_file = Path('ImageResults.txt').expanduser() if not table_file.exists(): t = QTable() t['Filename'] = [f.name] t['EL'] = [EL.value] t['PA'] = [SKYPA2.value] t['RotAng'] = [ROTPPOSN.value] t['GuiderCoord'] = [guider_coord.to_string(style='hmsdms', sep=':', precision=2)] t['ImageCoord'] = [center_coord.to_string(style='hmsdms', sep=':', precision=2)] t['OffsetDistance'] = [offset_distance.value] t['OffsetAngle'] = [offset_angle.value] print(t) else: t = QTable.read(table_file, format='ascii.ecsv') row = {'Filename': f.name, 'EL': EL.value, 'PA': SKYPA2.value, 'RotAng': ROTPPOSN.value, 'GuiderCoord': guider_coord.to_string(style='hmsdms', sep=':', precision=2), 'ImageCoord': center_coord.to_string(style='hmsdms', sep=':', precision=2), 'OffsetDistance': offset_distance.value, 'OffsetAngle': offset_angle.value, } t.add_row(vals=row) t.write(table_file, format='ascii.ecsv', overwrite=True)
'FILE', 'JD', 'RA (deg)', 'DEC (deg)', 'EXP (s)', 'FILTER', 'AIRMASS' ]) # loop over the files for imgfile in listfile: hdul = fits.open(imgfile) hdr = hdul[0].header # JD, the center coordinate (RA, Dec), exposure time, filter, airmass. JD = hdr['JD'] RAcenter = hdr['CRVAL1'] DEcenter = hdr['CRVAL2'] exptime = hdr['EXPOSURE'] filtr = hdr['FILTER'] airmass = hdr['AIRMASS'] data.add_row([ '201' + imgfile.replace(prefix, '').lstrip(), JD, RAcenter, DEcenter, exptime, filtr, airmass ]) t120.log.debug('File=' + imgfile + ' JD=' + str(JD) + ' RA=' + str(RAcenter) + ' DE=' + str(DEcenter) + ' exp=' + str(exptime) + ' fil=' + str(filtr) + ' airmass=' + str(airmass)) # remove first row data.remove_row(0) # save log in file ascii.write(data, fileout, format='fixed_width', delimiter=' ', formats={'JD': '%18.12f'}, overwrite=True) t120.log.info('There are ' + str(len(listfile)) + ' data saved in ' + fileout)
def calculate_one_night(self, night): """ For a given night, return the file counts and other other information for each exposure taken on that night input: night output: a dictionary containing the statistics with expid as key name FLAVOR: FLAVOR of this exposure OBSTYPE: OBSTYPE of this exposure EXPTIME: Exposure time SPECTROGRAPHS: a list of spectrographs used n_spectrographs: number of spectrographs n_psf: number of PSF files n_ff: number of fiberflat files n_frame: number of frame files n_sframe: number of sframe files n_cframe: number of cframe files n_sky: number of sky files """ output_arc = {} output_flat = {} output_science = {} fileglob_arc = os.path.join(self.prod_dir, 'run/scripts/night', str(night), 'arc*.log') fileglob_flat = os.path.join(self.prod_dir, 'run/scripts/night', str(night), 'flat*.log') fileglob_science = os.path.join(self.prod_dir, 'run/scripts/night', str(night), 'science*.log') file_arc = sorted(glob.glob(fileglob_arc)) file_flat = sorted(glob.glob(fileglob_flat)) file_science = sorted(glob.glob(fileglob_science)) table_output = QTable([[], [], [], [], []], names=('night', 'flavor', 'jobid', 'expid', 'time'), dtype=('S10', 'S10', 'S10', 'S10', 'float')) for file_this in file_arc: jobid_this = file_this.split('.')[0].split('-')[-1] expid_this = file_this.split('-')[2] result = os.popen('sacct -j ' + jobid_this + ' --format=Elapsed').read() time = result.split('\n')[-2].split(':') time_this = float(time[0]) * 60. + float( time[1]) + float(time[2]) / 60. table_output.add_row( [night, 'arc', jobid_this, expid_this, time_this]) #output_arc[jobid_this]={'expid':expid_this,'time':time_this} for file_this in file_flat: jobid_this = file_this.split('.')[0].split('-')[-1] expid_this = file_this.split('-')[2] result = os.popen('sacct -j ' + jobid_this + ' --format=Elapsed').read() time = result.split('\n')[-2].split(':') time_this = float(time[0]) * 60. + float( time[1]) + float(time[2]) / 60. table_output.add_row( [night, 'flat', jobid_this, expid_this, time_this]) #output_flat[jobid_this]={'expid':expid_this,'time':time_this} for file_this in file_science: jobid_this = file_this.split('.')[0].split('-')[-1] expid_this = file_this.split('-')[2] result = os.popen('sacct -j ' + jobid_this + ' --format=Elapsed').read() time = result.split('\n')[-2].split(':') time_this = float(time[0]) * 60. + float( time[1]) + float(time[2]) / 60. table_output.add_row( [night, 'science', jobid_this, expid_this, time_this]) #output_science[jobid_this]={'expid':expid_this,'time':time_this} return (table_output)
# Return components' mass halo_mass = ComponentMass(sys.argv[file], 1.) disk_mass = ComponentMass(sys.argv[file], 2.) bulge_mass = ComponentMass(sys.argv[file], 3.) # Add to local group mass group_halo += halo_mass group_disk += disk_mass group_bulge += bulge_mass # Calculate galaxy total mass and baryon fraction galaxy_mass = halo_mass + disk_mass + bulge_mass baryon_frac = np.around((disk_mass + bulge_mass) / galaxy_mass, 3) # Add a row to the table galaxy = [ galaxy_name, halo_mass, disk_mass, bulge_mass, galaxy_mass, baryon_frac ] gm_list.add_row(galaxy) # Edit the row of local group: total mass and baryon fraction group_mass = group_halo + group_disk + group_bulge group_fbar = np.around((group_disk + group_bulge) / group_mass, 3) galaxy_group = [ "Galaxy Group", group_halo, group_disk, group_bulge, group_mass, group_fbar ] gm_list.add_row(galaxy_group) # Convert the units into 1e12 M_sun gm_list[r'$M_{Halo}$'] = np.around(gm_list[r'$M_{Halo}$'].to(1e12 * u.solMass), 3) gm_list[r'$M_{Disk}$'] = np.around(gm_list[r'$M_{Disk}$'].to(1e12 * u.solMass), 3) gm_list[r'$M_{Bulge}$'] = np.around(
def makeobslog(path,root=''): """Make a journal of an observation night. Parameters ---------- path : string path where data are located. root : string, optional root of files to journal. Default is '', i.e. all files will be journaled. Returns ------- None. """ # set file name and pattern fileout = path+ 'journal' + root + '.log' pattern = path + root + '*.fit*' # make list of files from pattern listfile = glob.glob(pattern) nfile = len(listfile) # check number of files if (nfile==0): msg = '*** WARNING: there is no file of type: '+pattern t120.log.error(msg) raise IOError(msg) t120.log.info('There are '+str(nfile)+' files of type '+pattern) # get common prefix to all files prefix = os.path.commonprefix(listfile) # create output table: # method 1: obsolete and dumb... make a first dummy row so astropy.table.QTable knows 'FILTER' is a string #data = QTable([[' '],[' '], # [0.0],[0.0],[0.0],[0.0],[' '],[0.0]], # names=['FILE','TARGET','JD','RA (deg)','DEC (deg)','EXP (s)','FILTER','AIRMASS']) # method 2: declare types data = QTable(dtype=[object,object,float,float,float,float,object,float] names=['FILE','TARGET','JD','RA (deg)','DEC (deg)','EXP (s)','FILTER','AIRMASS']) # loop over the files for imgfile in listfile: hdul = fits.open(imgfile) hdr = hdul[0].header target_name = hdr['OBJECT']#.strip().upper().replace(' ','') # JD, the center coordinate (RA, Dec), exposure time, filter, airmass. JD = hdr['JD'] try: RAcenter = hdr['CRVAL1'] DEcenter = hdr['CRVAL2'] except: t120.log.warning('There is no CRVAL keyword in file '+imgfile) skycoo = SkyCoord(hdr['OBJCTRA'],hdr['OBJCTDEC'], unit=(u.hourangle, u.deg)) RAcenter = skycoo.ra.to('deg').value DEcenter = skycoo.dec.to('deg').value exptime = hdr['EXPOSURE'] filtr = hdr['FILTER'] airmass = hdr['AIRMASS'] data.add_row([imgfile.replace(prefix,'').lstrip(),target_name,JD,RAcenter,DEcenter,exptime,filtr,airmass]) t120.log.debug('File='+imgfile+' TARGET'+target_name+' JD='+str(JD)+' RA='+str(RAcenter)+' DE='+str(DEcenter)+ ' exp='+str(exptime)+' fil='+str(filtr)+' airmass='+str(airmass)) # remove first row : useful if method 1 is used: obsolete and dumb... #data.remove_row(0) # save log in file ascii.write(data,fileout,format='fixed_width',delimiter=' ',formats={'JD': '%18.12f'},overwrite=True) t120.log.info('There are '+str(len(listfile))+' data saved in '+fileout) return
def proc_lmon_full(merged_summaries_file: str, inst_mode: str = 'FF', xline: str = 'Cu-Ka', data_dir: str = "./", output_file: str = "output.csv",verbose: bool =True) -> pd.DataFrame: """ Merge all lmon fit results for a given line and mode, for each CCD and the full RAWY range (1,200) Input Parameters: df : DataFrame Pandas DataFrame with the merged summaries, will be used to select the OBS_IDs for the `xmode` inst_mode : str The instrument mode, can be 'FF' or 'EFF' xline : str The label of the line, as in the output files from Michael, can be 'Cu-Ka', 'Mn-Ka' or 'Al-Ka' data_dir : str The absolute path to the monitoring results folder, for example "/xdata/xcaldata/XMM/PN/CTI/dat_0062_sci" output_file : str The absolute path to the output file where the results will be saved verbose : bool If True will print some verbose info Output: the merged results in a pandas DataFrame """ # # lines of interest and their rest-frame energies in eV # lines0 = {'Cu-Ka': 8038.0, 'Mn-Ka': 5899.0, 'Al-Ka': 1486.0} # if (not os.path.isfile(merged_summaries_file)): print (f'File with merged summaries {merged_summaries_file} not found. Cannot continue.') return None # if (not os.path.isdir(data_dir)): print (f'Data dir {data_dir} not found. Cannot continue.') return None # if (xline not in lines0.keys()): print (f"Line {xline} not in list with rest-frame energy, please add it and run again.") return None # if (inst_mode == 'EFF'): select_mode = "PrimeFullWindowExtended" elif (inst_mode == 'FF'): select_mode = "PrimeFullWindow" else: print ('Only inst_mode=\'FF\' or \'EFF\' supported') return None df = QTable.read(merged_summaries_file) # df.sort('rev') # df = df[df['mode'] == select_mode] # nt = len(df) print (f"Will process {len(df)} {inst_mode} mode observations") print ("Doing line",xline) # all_cols = ['obsid','expo_name','rev','delta_time','omode','filter','expo_time','ccd', 'mipsel','maxmip','med_ndl','ndl','ndl_err'] # from meanXY.txt file all_cols.extend(["rawx0","rawx1","rawy0","rawy1","rawx_mean","rawx_med","rawx_16","rawx_84", "rawy_mean","rawy_med","rawy_16","rawy_84","nevents"]) # feom lmon file all_cols.extend(["energy","energy_lo","energy_hi", "sigma","sigma_lo","sigma_hi","area","area_lo","area_hi", "pw_order","pw_slope","pw_slope_lo","pw_slope_hi","pw_norm","pw_norm_lo","pw_norm_hi", "fit_flag0","fit_flag1","fit_flag2","fit_flag3","fit_stat","dof"]) # bore_ccds = [1,4,7,10] # set up the CCD numbers and the corresponding quadrants quad = {1: '0', 2: '0', 3: '0', 4: '1', 5: '1', 6: '1', 7: '2', 8: '2', 9: '2', 10: '3', 11: '3', 12: '3',} # t = QTable(names=all_cols, dtype=[int, str, int, float, str, str, float, int, int, int, float, float, float, int, int, int, int, float, float, float, float, float, float, float, float, int, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, int, int, int, int, float, int]) #print (t.colnames) # start_time = time.time() # for i in tqdm(range(nt),desc='Processing obs'): iobs = df['obsid'][i] irev = df['rev'][i] inexp = df['expid'][i] istart = df['tstart'][i] iexpo = df['texpo'][i] imode = df['mode'][i] ifilter = df['filt'][i] # stime = datetime.strptime(istart,"%Y-%m-%dT%H:%M:%S") delta_time = (stime-time0).total_seconds()/(365.0*24.0*3600.0) # in years # part1 = [iobs,inexp,irev,delta_time,imode,ifilter,iexpo] # part2 = [] for iccd in np.arange(1,13): # part2 = part1.copy() resfile = f"{data_dir}/{iobs:010}/{iobs:010}{inexp}*lmonCCD{iccd:02}_{xline}.txt" xfile = glob.glob(resfile) # need to do this as there is inconsistency in the file names axline = xline.split('-')[0] # _meanXY.txt rawxy_file1 = f"{data_dir}/{iobs:010}/{iobs:010}{inexp}*lmonCCD{iccd:02}_{axline}_meanXY.txt" file_means = glob.glob(rawxy_file1) # # check if this CCD has results # if ((len(xfile) < 1) or (len(file_means) < 1)): print (f'No data for RAWY in (1,200) for CCD {iccd}, {iobs:010}') continue # now add quadrant specific parameters ndl = df[f'ndisclin_mean{quad[iccd]}'][i] med_ndl = df[f'ndisclin_med{quad[iccd]}'][i] ndl_err = df[f'ndisclin_std{quad[iccd]}'][i] mipsel = df[f'mipsel{quad[iccd]}'][i] maxmip = df[f'maxmip{quad[iccd]}'][i] # part2.extend([iccd,mipsel,maxmip,med_ndl,ndl,ndl_err]) # # read the meanXY file as text # out1_line = [] with open(file_means[0],'r') as mm: qlines = mm.readlines() for qline in qlines: qx = qline.split() if ((qx[2] == '1') and (qx[3] == '200')): out1_line = qx break # skip if no results for RAWY (1,200) are available if (len(out1_line) < 1): print (f'No meanXY data for RAWY in (1,200) for CCD {iccd}') continue # out2_line = [] with open(xfile[0],'r') as mm: qlines = mm.readlines() for qline in qlines: qx = qline.split() if ((qx[2] == '1') and (qx[3] == '200')): out2_line = qx break # # skip if no results for RAWY (1,200) are available if (len(out2_line) < 1): print (f'No lmon data for RAWY in (1,200) for CCD {iccd}') continue # # now extend the array # out1_line.extend(out2_line[4:]) part2.extend(out1_line) # t.add_row(part2) # tx = _convert_adu(t) # return t
def group_table(self, edges): """Compute bin groups table for the map axis, given coarser bin edges. Parameters ---------- edges : `~astropy.units.Quantity` Group bin edges. Returns ------- groups : `~astropy.table.Table` Map axis group table. """ # TODO: try to simplify this code if not self.node_type == "edges": raise ValueError("Only edge based map axis can be grouped") edges_pix = self.coord_to_pix(edges) edges_pix = np.clip(edges_pix, -0.5, self.nbin - 0.5) edges_idx = np.round(edges_pix + 0.5) - 0.5 edges_idx = np.unique(edges_idx) edges_ref = self.pix_to_coord(edges_idx) groups = QTable() groups[f"{self.name}_min"] = edges_ref[:-1] groups[f"{self.name}_max"] = edges_ref[1:] groups["idx_min"] = (edges_idx[:-1] + 0.5).astype(int) groups["idx_max"] = (edges_idx[1:] - 0.5).astype(int) if len(groups) == 0: raise ValueError("No overlap between reference and target edges.") groups["bin_type"] = "normal " edge_idx_start, edge_ref_start = edges_idx[0], edges_ref[0] if edge_idx_start > 0: underflow = { "bin_type": "underflow", "idx_min": 0, "idx_max": edge_idx_start, f"{self.name}_min": self.pix_to_coord(-0.5), f"{self.name}_max": edge_ref_start, } groups.insert_row(0, vals=underflow) edge_idx_end, edge_ref_end = edges_idx[-1], edges_ref[-1] if edge_idx_end < (self.nbin - 0.5): overflow = { "bin_type": "overflow", "idx_min": edge_idx_end + 1, "idx_max": self.nbin - 1, f"{self.name}_min": edge_ref_end, f"{self.name}_max": self.pix_to_coord(self.nbin - 0.5), } groups.add_row(vals=overflow) group_idx = Column(np.arange(len(groups))) groups.add_column(group_idx, name="group_idx", index=0) return groups
def table_pyoof_out(path_pyoof_out, order): """ Auxiliary function to tabulate all data from a series of observations gathered in a common ``pyoof_out/`` directory. Note: Piston and tilt are not used in error calculations, the phase calculations will only be included if these are manually changed in ``core.py``. Parameters ---------- path_pyoof_out : `list` set of paths to the directory ``pyoof_out/`` or where the output from the `~pyoof` package is located. order : `int` Order used for the Zernike circle polynomial, :math:`n`. Returns ------- qt : `~astropy.table.table.QTable` `~astropy.table.table.QTable` with units of the most important quantities from the `~pyoof` package. """ qt = QTable(names=[ 'name', 'tel_name', 'obs-object', 'obs-date', 'meanel', 'i_amp', 'c_dB', 'q', 'phase-rms', 'e_rs', 'beam-snr-out-l', 'beam-snr-in', 'beam-snr-out-r' ], dtype=[np.string_] * 4 + [np.float] * 9) for p, pyoof_out in enumerate(path_pyoof_out): with open(os.path.join(pyoof_out, 'pyoof_info.yml'), 'r') as inputfile: pyoof_info = yaml.load(inputfile, Loader=yaml.Loader) _phase = np.genfromtxt(os.path.join(pyoof_out, f'phase_n{order}.csv')) * apu.rad phase_rms = rms(_phase, circ=True) phase_e_rs = e_rs(_phase, circ=True) # random-surface-error efficiency error # cov = np.genfromtxt(os.path.join(pyoof_out, f'cov_n{order}.csv')) # idx = np.argwhere(cov[0, :].astype(int) > 5) params = Table.read(os.path.join(pyoof_out, f'fitpar_n{order}.csv'), format='ascii') I_coeff = params['parfit'][:5] qt.add_row([ pyoof_info['name'], pyoof_info['tel_name'], pyoof_info['obs_object'], pyoof_info['obs_date'], pyoof_info['meanel'], I_coeff[0], I_coeff[1], I_coeff[2], phase_rms, phase_e_rs ] + pyoof_info['snr']) # updating units qt['phase-rms'] *= apu.rad qt['meanel'] *= apu.deg qt['obs-date'] = Time(qt['obs-date'], format='isot', scale='utc') qt['c_dB'] *= apu.dB qt.meta = {'order': order} return qt
def iontable_from_components(components, ztbl=None): """Generate a QTable from a list of components Method does *not* perform logic on redshifts or vlim. Includes rules for adding components of like ion Not ready for varying atomic mass (e.g. Deuterium) Parameters ---------- components : list list of AbsComponent objects ztbl : float, optional Redshift for the table Returns ------- iontbl : QTable """ from collections import OrderedDict # Checks assert chk_components(components,chk_A_none=True) # Set z from mean if ztbl is None: ztbl = np.mean([comp.zcomp for comp in components]) # Construct the QTable cols = OrderedDict() # Keeps columns in order cols['Z']=int cols['ion']=int cols['A']=int cols['Ej']=float cols['z']=float cols['vmin']=float cols['vmax']=float cols['flag_N']=int cols['logN']=float cols['sig_logN']=float names = cols.keys() dtypes = [cols[key] for key in names] iontbl = QTable(names=names,dtype=dtypes) iontbl['vmin'].unit=u.km/u.s iontbl['vmax'].unit=u.km/u.s # Identify unique Zion, Ej (not ready for A) uZiE = np.array([comp.Zion[0]*1000000+comp.Zion[1]*10000+ comp.Ej.to('1/cm').value for comp in components]) uniZi, auidx = np.unique(uZiE, return_index=True) # Loop for uidx in auidx: # Synthesize components with like Zion, Ej mtZiE = np.where(uZiE == uZiE[uidx])[0] comps = [components[ii] for ii in mtZiE] # Need a list synth_comp = synthesize_components(comps, zcomp=ztbl) # Add a row to QTable row = dict(Z=synth_comp.Zion[0],ion=synth_comp.Zion[1], z=ztbl, Ej=synth_comp.Ej,vmin=synth_comp.vlim[0], vmax=synth_comp.vlim[1],logN=synth_comp.logN, flag_N=synth_comp.flag_N,sig_logN=synth_comp.sig_logN) iontbl.add_row(row) # Add zlim to metadata meta = OrderedDict() meta['zcomp'] = ztbl # Return return iontbl
def estimate_extreme_velocities(self, threshold, source_distance, plot=False, weak_quadrants=False, velocity_interval=None, channel_interval=None, writeto=None, debug=False): # initialize the data table table = QTable(names=('Position', 'Channel', 'Angular distance', 'Distance', 'Velocity'), dtype=(int, int, u.Quantity, u.Quantity, u.Quantity)) # estimate the extreme channels if velocity_interval is not None: if isinstance(velocity_interval, tuple): channel_interval = self._velocity_to_channel(velocity_interval) else: raise TypeError( 'The function estimate_extreme_velocities() can only handle a single velocity interval at a time but got {}!' .format(velocity_interval)) self.estimate_extreme_channels(threshold, plot=False, weak_quadrants=weak_quadrants, channel_interval=channel_interval) elif channel_interval is not None: self.estimate_extreme_channels(threshold, plot=False, weak_quadrants=weak_quadrants, channel_interval=channel_interval) else: self.estimate_extreme_channels(threshold, plot=False, weak_quadrants=weak_quadrants) # transfer the channels into physical units for position, channel in enumerate(self.channels): angular_distance = ( position - self.position_reference) * self.position_resolution distance = self._angle_to_length(angular_distance, source_distance) velocity = (channel - self.vLSR_channel ) * self.velocity_resolution + self.vLSR try: table.add_row([ position, channel, angular_distance.value, distance.value, velocity.value ]) except AttributeError: # print([position, channel, angular_distance, distance, velocity]) pass table['Angular distance'] = table[ 'Angular distance'] * self.position_resolution.unit table['Distance'] = table['Distance'] * u.AU table['Velocity'] = table['Velocity'] * self.velocity_resolution.unit # plot if plot: plt.plot(table['Distance'], table['Velocity'], 'o', label='data') plt.xlabel('Position offest ({})'.format(table['Distance'].unit)) plt.xlabel('Velocity ({})'.format(table['Velocity'].unit)) plt.axhline(self.vLSR.value, c='k', ls='--', label='$v_\mathrm{LSR}$') plt.grid() plt.legend() plt.show() plt.close() if debug: print('Estimates of extreme velocities:') print(table) if writeto: table.write(writeto, format='ascii.fixed_width', overwrite=True) return table