Source code for sora.occultation.chordlist

import warnings

import numpy as np

from sora.config.list import List
from .chord import Chord

__all__ = ['ChordList']


[docs] class ChordList(List): """Defines a collection of Chord objects associated to an Occultation. This object is not supposed to be defined by the user. It will be automatically defined in Occultation. Parameters ---------- star : `sora.Star` The occulted star. body : `sora.Body` The occulting body. time : `astropy.time.Time` The occultation time. """ _allowed_types = (Chord,) # (Chord, AstrometricChord) _set_func = 'add_chord' def __init__(self, *, star, body, time): super().__init__() self._star = star self._body = body self._time = time self._shared_with = {"chord": {"star": self._star, "ephem": self._body.ephem, "time": self._time}, 'occultation': {}} self._method_value = 'observer'
[docs] def add_chord(self, *, name=None, chord=None, observer=None, lightcurve=None): """Adds a chord to the occultation chord list. Parameters ---------- name : `str`, optional The name of the Chord. It must be unique within the list of chords. If not given, it will use the name in chord if chord is directly given or the name in the observer object. If the name in Chord already exists in the list, the name parameter can be given to update the chord name. The `name` parameter must be provided when given together with observer and lightcurve. It cannot be an empty string. chord : `sora.occultation.Chord`, optional A chord instance defined by the user. observer : `sora.Observer`, optional An Observer instance defined by the user. It must be given together with a LightCurve. lightcurve : `sora.LightCurve`, optional A LightCurve defined by the user. It must be given together with observer. Returns ------- chord : `sora.occultation.Chord` The chord added to the list. Examples -------- Ways to add a chord: >>> obj.add_chord(chord=chord) # where the name in chord will be used. >>> obj.add_chord(name=name, chord=chord) # where the name in chord will be replaced by "name". >>> obj.add_chord(observer=observer, lightcurve=lightcurve) # where the name in observer will be used. >>> obj.add_chord(name=name, observer=observer, lightcurve=lightcurve) """ if chord and (observer or lightcurve): raise ValueError("User must give only chord or (name and observer and lightcurve)") if chord: name = name or chord.name elif observer and lightcurve: for key in self.keys(): if self[key].lightcurve is lightcurve: raise ValueError('lightcurve is already associated to the chord {}'.format(key)) name = name or observer.name chord = Chord(name=name, observer=observer, lightcurve=lightcurve) else: raise ValueError("User must give chord or (name and observer and lightcurve)") self._add_item(name=name, item=chord) chord._name = name chord._shared_with["chordlist"] = self._shared_with["chord"] if not hasattr(chord.lightcurve, 'vel'): chord.lightcurve.set_vel(self._shared_with['occultation']["vel"]) if not hasattr(chord.lightcurve, 'dist'): chord.lightcurve.set_dist(self._shared_with['occultation']["dist"]) if not hasattr(chord.lightcurve, 'd_star'): chord.lightcurve.set_star_diam(self._shared_with['occultation']["star_diam"]) try: chord.lightcurve.calc_magnitude_drop(mag_star=self._star.mag['G'], mag_obj=self._body.apparent_magnitude(self._time)) except: chord.lightcurve.bottom_flux = 0.0 warnings.warn('Magnitude drop was not calculated. Using bottom flux as 0.0.') chord._method = self._method return chord
# This attribute is to modify the way to calculate f and g on chord. @property def _method(self): return self._method_value @_method.setter def _method(self, value): if value not in ['geocenter', 'observer']: raise ValueError('method must be "geocenter" or "observer"') self._method_value = value for name, chord in self.items(): chord._method = value
[docs] def remove_chord(self, *, name): """Removes a chord from the chord list and disassociates it from the Occultation. Parameters ---------- name : `str` The name of the chord. """ del(self[name]._shared_with['chordlist']) del(self[name])
@property def is_able(self): return {name: chord.is_able for name, chord in self.items()}
[docs] def enable(self, *, chord=None, time=None): """Enables a contact point of the curve to be used in the fit. Parameters ---------- chord : `str`, optional Name of the chord to enable. If ``chord=None``, it applies to all chords. time : `None`, `str` If ``time=None``, it will enable all contact points. If ``'immersion'`` or ``'emersion'``, it will enable the respective contact point. """ n = 0 for key in self.keys(): if chord is None or chord == key: self[key].enable(time=time) n += 1 if n == 0: raise ValueError('No Chord with name {} is available.'.format(chord))
[docs] def disable(self, *, chord=None, time=None): """Disables a contact point of the curve to be used in the fit. Parameters ---------- chord : `str`, optional Name of the chord to disable. If ``chord=None``, it applies to all chords. time : `None`, `str` If ``time=None``, it will disable all contact points. If ``'immersion'`` or ``'emersion'``, it will disable the respective contact point. """ n = 0 for key in self.keys(): if chord is None or chord == key: self[key].disable(time=time) n += 1 if n == 0: raise ValueError('No Chord with name {} is available.'.format(chord))
[docs] def plot_chords(self, *, segment='standard', ignore_chords=None, only_able=False, ax=None, linestyle='-', **kwargs): """Plots the on-sky path for the chords in this list. Parameters ---------- segment : `str` The segment to plot the chord. The available options are: ``'positive'`` to get the path between the immersion and emersion times if the chord is positive. ``'negative'`` to get the path between the start and end of observation if the chord is negative. ``'standard'`` to get the 'positive' path if the chord is positive or 'negative' if the chord is negative. ``'full'`` to get the path between the start and end of observation independently of whether the chord is positive or negative. ``'outer'`` to get the path outside the 'positive' path, for instance between the start and immersion times and between the emersion and end times. ``'error'`` to get the path corresponding to the error bars. ignore_chords : `str`, `list`, optional Name of chord or list of names to ignore in the plot. only_able : `bool`, default=False Plot only the chords or contact points that are able to be used in the fit. If ``segment='error'`` it will show only the contact points able. If segment is any other, the path will be plotted only if both immersion and emersion are able, or it is a negative chord. ax : `matplotlib.pyplot.Axes`, optional The axes where to make the plot. If None, it will use the default axes. linestyle : `str`, default='-' Default linestyle used in `matplotlib.pyplot.plot`. The difference is that now it accepts ``linestyle='exposure'``, where the plot will be a dashed line corresponding to each exposure. The blank space between the lines can be interpreted as 'dead time'. **kwargs Any other kwarg will be parsed directly by `matplotlib.pyplot.plot`. The only difference is that the default linewidth ``lw=2``. """ n = 0 keys = list(self.keys()) if ignore_chords is not None: ignore_chords = np.array(ignore_chords, ndmin=1) for i in range(len(self)): if ignore_chords is not None and keys[i] in ignore_chords: continue label = None if segment != 'error': label = kwargs.get('label', keys[i]) try: self[i].plot_chord(segment=segment, only_able=only_able, ax=ax, linestyle=linestyle, label=label, **kwargs) except ValueError: n += 1 if n == len(self): warnings.warn('Segment "{}" was not found on any chord'.format(segment))
[docs] def summary(self): """Prints a table with the summary of the chords. """ from astropy.table import Table, vstack from astropy.time import Time tables = [] for key in self.keys(): tt = Table() chord = self[key] obs = chord.observer lc = chord.lightcurve max_val = 0 cols = [] colnames = ['Name', 'Longitude', 'Latitude', 'status', 'time', 'f', 'g'] itens = [[key], [obs.lon.to_string()], [obs.lat.to_string()]] labels = [] vals = [] times = [('Initial Time', 'initial_time'), ('Immersion', 'immersion'), ('Emersion', 'emersion'), ('End Time', 'end_time')] for label, attr in times: val = getattr(lc, attr, None) if val is not None: labels.append(label) vals.append(val) if len(vals) > 0: f, g = chord.get_fg(time=Time(vals)) f = np.array(f, ndmin=1) g = np.array(g, ndmin=1) itens += [ labels, [val.iso for val in vals], ['{:.2f}'.format(n) for n in f], ['{:.2f}'.format(n) for n in g], ] for item in itens: v = np.array(item, ndmin=1).tolist() cols.append(v) if len(v) > max_val: max_val = len(v) for i in range(len(cols)): if len(cols[i]) < max_val: for n in range(max_val - len(cols[i])): cols[i].append('') tt[colnames[i]] = cols[i] tables.append(tt) tabela = vstack(tables) tabela.pprint_all()
[docs] def get_impact_param(self, chords='all_chords', center_f=0, center_g=0, verbose=True): """Gets the impact parameter, the minimum distance between each chord and the center position. This ChordList object must be associated to an Occultation to work, since it needs the position of the star and an ephemeris. Parameters ---------- chords : `int`, `str`, iterable, default='all_chords' Indices or names of the chords to be considered. If ``'all_chords'``, all chords are used. center_f : `int`, `float`, default=0 The coordinate in f of the ellipse center. center_g : `int`, `float`, default=0 The coordinate in g of the ellipse center. verbose : `bool`, default=True If True, prints the obtained values. Returns ------- impact, sense, chord_name : `list` The impact parameter (in km), the direction of the chord relative to the ellipse center, North (N), South (S), East (E) and West (W), and the name of the chord. """ impact = np.array([]) sense = np.array([]) names = np.array([]) if chords == 'all_chords': chords = range(len(self)) for i in chords: chord = self[i] im, se = chord.get_impact_param(center_f=center_f, center_g=center_g, verbose=verbose) impact = np.append(impact, im) sense = np.append(sense, se) names = np.append(names, chord.name) return impact, sense, names
[docs] def get_theoretical_times(self, equatorial_radius, chords='all_chords', center_f=0, center_g=0, oblateness=0, position_angle=0, sigma=0, step=1, verbose=True): """Gets the theoretical times and chord sizes for a given ellipse. This ChordList object must be associated to an Occultation to work, since it needs the position of the star and an ephemeris. Parameters ---------- chords : `int`, `str`, iterable, default='all_chords' Indices or names of the chords to be considered. If ``'all_chords'``, all chords are used. equatorial_radius : `int`, `float` The equatorial radius (semi-major axis) of the ellipse. center_f : `int`, `float`, default=0 The coordinate in f of the ellipse center. center_g : `int`, `float`, default=0 The coordinate in g of the ellipse center. oblateness : `int`, `float`, default=0 The oblateness of the ellipse. position_angle : `int`, `float`, default=0 The pole position angle of the ellipse in degrees. Zero is in the North direction ('g-positive'). Positive clockwise. sigma : `int`, `float`, default=0 Uncertainty of the expected ellipse, in km. step : `int`, `float`, default=1 Time resolution of the chord, in seconds. verbose : `bool`, default=True If True, prints the obtained values. Returns ------- theory_immersion_time, theory_emersion_time, theory_chord_size, chord_name : `list` The expected immersion time for the given ellipse, the expected emersion time for the given ellipse, the expected chord size for the given ellipse, and the name of the chord. """ theory_immersion_time = np.array([]) theory_emersion_time = np.array([]) theory_chord_size = np.array([]) names = np.array([]) if chords == 'all_chords': chords = range(len(self)) for i in chords: chord = self[i] tit, tet, tcs = chord.get_theoretical_times(equatorial_radius=equatorial_radius, center_f=center_f, center_g=center_g, oblateness=oblateness, position_angle=position_angle, sigma=sigma, step=step, verbose=verbose) theory_immersion_time = np.append(theory_immersion_time, tit) theory_emersion_time = np.append(theory_emersion_time, tet) theory_chord_size = np.append(theory_chord_size, tcs) names = np.append(names, chord.name) return theory_immersion_time, theory_emersion_time, theory_chord_size, names
[docs] def get_limb_points(self, only_able=True): """Computes projected points and errors on the tangent plane for every chord. Parameters ---------- only_able : `bool`, default=True Get only the contact points that are able to be used in the fit. Returns ------- names : `numpy.array` Names of the projected contact points. fg : `numpy.array` The projected points of occultation instants. Each line is a point on the projection with x and y respectively. error : `numpy.array` Error vector of the projected occultation instants. Each line is the error of a point in x and y, respectively. """ names = [] xy = [] err = [] for chord in self.values(): if chord.status() == 'negative': continue name_chord, xy_chord, err_chord = chord.get_limb_points(only_able=only_able) if len(name_chord) == 0: continue names.append(name_chord) xy.append(xy_chord) err.append(err_chord) return np.hstack(names), np.vstack(xy), np.vstack(err)