text: """Defines the Seismology class."""
  import logging
  import warnings
  
  import numpy as np
  from matplotlib import pyplot as plt
  from scipy.signal import find_peaks
  
  from astropy import units as u
  from astropy.units import cds
  
  from .. import MPLSTYLE
  from . import utils, stellar_estimators
  from ..periodogram import SNRPeriodogram
  from ..utils import LightkurveWarning, validate_method
  from .utils import SeismologyQuantity
  
  # Import the optional Bokeh dependency required by ``interact_echelle```,
  # or print a friendly error otherwise.
  try:
      import bokeh  # Import bokeh first so we get an ImportError we can catch
      from bokeh.io import show, output_notebook
      from bokeh.plotting import figure
      from bokeh.models import LogColorMapper, Slider, RangeSlider, Button
      from bokeh.layouts import layout, Spacer
  except:
      # Nice error will be raised when ``interact_echelle``` is called.
      pass
  
  log = logging.getLogger(__name__)
  
  __all__ = ["Seismology"]
  
  
  class Seismology(object):
      """Enables astroseismic quantities to be estimated from periodograms.
  
      This class provides easy access to methods to estimate numax, deltanu, radius,
      mass, and logg, and stores them on its tray for easy diagnostic plotting.
  
      Examples
      --------
      Download the TESS light curve for HIP 116158:
  
          >>> import lightkurve as lk
          >>> lc = lk.search_lightcurve("HIP 116158", sector=2).download()  # doctest: +SKIP
          >>> lc = lc.normalize().remove_nans().remove_outliers()  # doctest: +SKIP
  
      Create a Lomb-Scargle periodogram:
  
          >>> pg = lc.to_periodogram(normalization='psd', minimum_frequency=100, maximum_frequency=800)  # doctest: +SKIP
  
      Create a Seismology object and use it to estimate parameters:
  
          >>> seismology = pg.flatten().to_seismology()  # doctest: +SKIP
          >>> seismology.estimate_numax()  # doctest: +SKIP
          numax: 415.00 uHz (method: ACF2D)
          >>> seismology.estimate_deltanu()  # doctest: +SKIP
          deltanu: 28.78 uHz (method: ACF2D)
          >>> seismology.estimate_radius(teff=5080)  # doctest: +SKIP
          radius: 2.78 solRad (method: Uncorrected Scaling Relations)
  
      Parameters
      ----------
      periodogram : `~lightkurve.periodogram.Periodogram` object
          Periodogram to be analyzed. Must be background-corrected,
          e.g. using `periodogram.flatten()`.
      """
  
      periodogram = None
      """The periodogram from which seismological parameters are being extracted."""
  
      def __init__(self, periodogram):
          if not isinstance(periodogram, SNRPeriodogram):
              warnings.warn(
                  "Seismology received a periodogram which does not appear "
                  "to have been background-corrected. Please consider calling "
                  "`periodogram.flatten()` prior to extracting seismological parameters.",
                  LightkurveWarning,
              )
          self.periodogram = periodogram
  
      def __repr__(self):
          attrs = np.asarray(["numax", "deltanu", "mass", "radius", "logg"])
          tray = np.asarray([hasattr(self, attr) for attr in attrs])
          if tray.sum() == 0:
              tray_str = " - no values have been computed so far."
          else:
              tray_str = " - computed values:\n * " + "\n * ".join(
                  [getattr(self, attr).__repr__() for attr in attrs[tray]]
              )
          return "Seismology(ID: {}){}".format(self.periodogram.label, tray_str)
  
      @staticmethod
      def from_lightcurve(lc, **kwargs):
          """Returns a `Seismology` object given a `LightCurve`."""
          log.info(
              "Building a Seismology object directly from a light curve "
              "uses default periodogram parameters. For further tuneability, "
              "create a periodogram object first, using `to_periodogram`."
          )
          return Seismology(
              periodogram=lc.normalize()
              .remove_nans()
              .fill_gaps()
              .to_periodogram(**kwargs)
              .flatten()
          )
  
      def _validate_numax(self, numax):
          """Raises exception if `numax` is None and `self.numax` is not set."""
          if numax is None:
              try:
                  return self.numax
              except AttributeError:
                  raise AttributeError(
                      "You need to call `Seismology.estimate_numax()` first."
                  )
          return numax
  
      def _validate_deltanu(self, deltanu):
          """Raises exception if `deltanu` is None and `self.deltanu` is not set."""
          if deltanu is None:
              try:
                  return self.deltanu
              except AttributeError:
                  raise AttributeError(
                      "You need to call `Seismology.estimate_deltanu()` first."
                  )
          return deltanu
  
      def _clean_echelle(
          self,
          deltanu=None,
          numax=None,
          minimum_frequency=None,
          maximum_frequency=None,
          smooth_filter_width=0.1,
          scale="linear",
      ):
          """Takes input seismology object and creates the necessary arrays for an echelle
          diagram. Validates all the inputs.
  
          Parameters
          ----------
          deltanu : float
              Value for the large frequency separation of the seismic mode
              frequencies in the periodogram. Assumed to have the same units as
              the frequencies, unless given an Astropy unit.
              Is assumed to be in the same units as frequency if not given a unit.
          numax : float
              Value for the frequency of maximum oscillation. If a numax is
              passed, a suitable range one FWHM of the mode envelope either side
              of the will be shown. This is overwritten by custom frequency ranges.
              Is assumed to be in the same units as frequency if not given a unit.
          minimum_frequency : float
              The minimum frequency at which to display the echelle
              Is assumed to be in the same units as frequency if not given a unit.
          maximum_frequency : float
              The maximum frequency at which to display the echelle.
              Is assumed to be in the same units as frequency if not given a unit.
          smooth_filter_width : float
              If given a value, will smooth periodogram used to plot the echelle
              diagram using the periodogram.smooth(method='boxkernel') method with
              a filter width of `smooth_filter_width`. This helps visualise the
              echelle diagram. Is assumed to be in the same units as the
              periodogram frequency.
          ax : `~matplotlib.axes.Axes`
              A matplotlib axes object to plot into. If no axes is provided,
              a new one will be created.
          scale: str
              Set z axis to be "linear" or "log". Default is linear.
  
          Returns
          -------
          ep : np.ndarray
              Echelle diagram power
          x_f : np.ndarray
              frequencies for X axis
          y_f : np.ndarray
              frequencies for Y axis
          """
          if (minimum_frequency is None) & (maximum_frequency is None):
              numax = self._validate_numax(numax)
          deltanu = self._validate_deltanu(deltanu)
  
          if (not hasattr(numax, "unit")) & (numax is not None):
              numax = numax * self.periodogram.frequency.unit
          if (not hasattr(deltanu, "unit")) & (deltanu is not None):
              deltanu = deltanu * self.periodogram.frequency.unit
  
          if smooth_filter_width:
              pgsmooth = self.periodogram.smooth(filter_width=smooth_filter_width)
              freq = pgsmooth.frequency  # Makes code below more readable below
              power = pgsmooth.power  # Makes code below more readable below
          else:
              freq = self.periodogram.frequency  # Makes code below more readable
              power = self.periodogram.power  # Makes code below more readable
  
          fmin = freq[0]
          fmax = freq[-1]
  
          # Check for any superfluous input
          if (numax is not None) & (
              any([a is not None for a in [minimum_frequency, maximum_frequency]])
          ):
              warnings.warn(
                  "You have passed both a numax and a frequency limit. "
                  "The frequency limit will override the numax input.",
                  LightkurveWarning,
              )
  
          # Ensure input numax is in the correct units (if there is one)
          if numax is not None:
              numax = u.Quantity(numax, freq.unit).value
              if numax > freq[-1].value:
                  raise ValueError(
                      "You can't pass in a numax outside the"
                      "frequency range of the periodogram."
                  )
  
              fwhm = utils.get_fwhm(self.periodogram, numax)
  
              fmin = numax - 2 * fwhm
              if fmin < freq[0].value:
                  fmin = freq[0].value
  
              fmax = numax + 2 * fwhm
              if fmax > freq[-1].value:
                  fmax = freq[-1].value
  
          # Set limits and set them in the right units
          if minimum_frequency is not None:
              fmin = u.Quantity(minimum_frequency, freq.unit).value
              if fmin > freq[-1].value:
                  raise ValueError(
                      "You can't pass in a limit outside the "
                      "frequency range of the periodogram."
                  )
  
          if maximum_frequency is not None:
              fmax = u.Quantity(maximum_frequency, freq.unit).value
              if fmax > freq[-1].value:
                  raise ValueError(
                      "You can't pass in a limit outside the "
                      "frequency range of the periodogram."
                  )
  
          # Make sure fmin and fmax are Quantities or code below will break
          fmin = u.Quantity(fmin, freq.unit)
          fmax = u.Quantity(fmax, freq.unit)
  
          # Add on 1x deltanu so we don't miss off any important range due to rounding
          if fmax < freq[-1] - 1.5 * deltanu:
              fmax += deltanu
  
          fs = np.median(np.diff(freq))
          x0 = int(freq[0] / fs)
  
          ff = freq[int(fmin / fs) - x0 : int(fmax / fs) - x0]  # Selected frequency range
          pp = power[int(fmin / fs) - x0 : int(fmax / fs) - x0]  # Power range
  
          # Reshape the power into n_rows of n_columns
          #  When modulus ~ zero, deltanu divides into frequency without remainder
          mod_zeros = find_peaks(-1.0 * (ff % deltanu))[0]
  
          # The bottom left corner of the plot is the lowest frequency that
          # divides into deltanu with almost zero remainder
          start = mod_zeros[0]
  
          # The top left corner of the plot is the highest frequency that
          # divides into deltanu with almost zero remainder.  This index is the
          # approximate end, because we fix an integer number of rows and columns
          approx_end = mod_zeros[-1]
  
          # The number of rows is the number of times you can partition your
          #  frequency range into chunks of size deltanu, start and ending at
          #  frequencies that divide nearly evenly into deltanu
          n_rows = len(mod_zeros) - 1
  
          # The number of columns is the total number of frequency points divided
          #  by the number of rows, floor divided to the nearest integer value
          n_columns = int((approx_end - start) / n_rows)
  
          # The exact end point is therefore the ncolumns*nrows away from the start
          end = start + n_columns * n_rows
  
          ep = np.reshape(pp[start:end], (n_rows, n_columns))
  
          if scale == "log":
              ep = np.log10(ep)
  
          # Reshape the freq into n_rowss of n_columnss & create arays
          ef = np.reshape(ff[start:end], (n_rows, n_columns))
          x_f = (ef[0, :] - ef[0, 0]) % deltanu
          y_f = ef[:, 0]
          return ep, x_f, y_f
  
      def plot_echelle(
          self,
          deltanu=None,
          numax=None,
          minimum_frequency=None,
          maximum_frequency=None,
          smooth_filter_width=0.1,
          scale="linear",
          ax=None,
          cmap="Blues",
      ):
          """Plots an echelle diagram of the periodogram by stacking the
          periodogram in slices of deltanu.
  
          Modes of equal radial degree should appear approximately vertically aligned.
          If no structure is present, you are likely dealing with a faulty deltanu
          value or a low signal to noise case.
  
          This method is adapted from work by Daniel Hey & Guy Davies.
  
          Parameters
          ----------
          deltanu : float
              Value for the large frequency separation of the seismic mode
              frequencies in the periodogram. Assumed to have the same units as
              the frequencies, unless given an Astropy unit.
              Is assumed to be in the same units as frequency if not given a unit.
          numax : float
              Value for the frequency of maximum oscillation. If a numax is
              passed, a suitable range one FWHM of the mode envelope either side
              of the will be shown. This is overwritten by custom frequency ranges.
              Is assumed to be in the same units as frequency if not given a unit.
          minimum_frequency : float
              The minimum frequency at which to display the echelle
              Is assumed to be in the same units as frequency if not given a unit.
          maximum_frequency : float
              The maximum frequency at which to display the echelle.
              Is assumed to be in the same units as frequency if not given a unit.
          smooth_filter_width : float
              If given a value, will smooth periodogram used to plot the echelle
              diagram using the periodogram.smooth(method='boxkernel') method with
              a filter width of `smooth_filter_width`. This helps visualise the
              echelle diagram. Is assumed to be in the same units as the
              periodogram frequency.
          scale: str
              Set z axis to be "linear" or "log". Default is linear.
          cmap : str
              The name of the matplotlib colourmap to use in the echelle diagram.
  
          Returns
          -------
          ax : `~matplotlib.axes.Axes`
              The matplotlib axes object.
          """
          if (minimum_frequency is None) & (maximum_frequency is None):
              numax = self._validate_numax(numax)
          deltanu = self._validate_deltanu(deltanu)
  
          if (not hasattr(numax, "unit")) & (numax is not None):
              numax = numax * self.periodogram.frequency.unit
          if (not hasattr(deltanu, "unit")) & (deltanu is not None):
              deltanu = deltanu * self.periodogram.frequency.unit
  
          ep, x_f, y_f = self._clean_echelle(
              numax=numax,
              deltanu=deltanu,
              minimum_frequency=minimum_frequency,
              maximum_frequency=maximum_frequency,
              smooth_filter_width=smooth_filter_width,
          )
  
          # Plot the echelle diagram
          with plt.style.context(MPLSTYLE):
              if ax is None:
                  _, ax = plt.subplots()
              extent = (x_f[0].value, x_f[-1].value, y_f[0].value, y_f[-1].value)
              figsize = plt.rcParams["figure.figsize"]
              a = figsize[1] / figsize[0]
              b = (extent[3] - extent[2]) / (extent[1] - extent[0])
              vmin = np.nanpercentile(ep.value, 1)
              vmax = np.nanpercentile(ep.value, 99)
  
              im = ax.imshow(
                  ep.value,
                  cmap=cmap,
                  aspect=a / b,
                  origin="lower",
                  extent=extent,
                  vmin=vmin,
                  vmax=vmax,
              )
  
              cbar = plt.colorbar(im, ax=ax, extend="both", pad=0.01)
  
              if isinstance(self.periodogram, SNRPeriodogram):
                  ylabel = "Signal to Noise Ratio (SNR)"
              elif self.periodogram.power.unit == cds.ppm:
                  ylabel = "Amplitude [{}]".format(
                      self.periodogram.power.unit.to_string("latex")
                  )
              else:
                  ylabel = "Power Spectral Density [{}]".format(
                      self.periodogram.power.unit.to_string("latex")
                  )
  
              if scale == "log":
                  ylabel = "log10(" + ylabel + ")"
  
              cbar.set_label(ylabel)
              ax.set_xlabel(r"Frequency mod. {:.2f}".format(deltanu))
              ax.set_ylabel(
                  r"Frequency [{}]".format(
                      self.periodogram.frequency.unit.to_string("latex")
                  )
              )
              ax.set_title("Echelle diagram for {}".format(self.periodogram.label))
  
          return ax
  
      def _make_echelle_elements(
          self,
          deltanu,
          cmap="viridis",
          minimum_frequency=None,
          maximum_frequency=None,
          smooth_filter_width=0.1,
          scale="linear",
          plot_width=490,
          plot_height=340,
          title="Echelle",
      ):
          """Helper function to make the elements of the echelle diagram for bokeh plotting."""
          if not hasattr(deltanu, "unit"):
              deltanu = deltanu * self.periodogram.frequency.unit
  
          if smooth_filter_width:
              pgsmooth = self.periodogram.smooth(filter_width=smooth_filter_width)
              freq = pgsmooth.frequency  # Makes code below more readable below
          else:
              freq = self.periodogram.frequency  # Makes code below more readable
  
          ep, x_f, y_f = self._clean_echelle(
              deltanu=deltanu,
              minimum_frequency=minimum_frequency,
              maximum_frequency=maximum_frequency,
              smooth_filter_width=smooth_filter_width,
              scale=scale,
          )
  
          fig = figure(
              plot_width=plot_width,
              plot_height=plot_height,
              x_range=(0, 1),
              y_range=(y_f[0].value, y_f[-1].value),
              title=title,
              tools="pan,box_zoom,reset",
              toolbar_location="above",
              border_fill_color="white",
          )
  
          fig.yaxis.axis_label = r"Frequency [{}]".format(freq.unit.to_string())
          fig.xaxis.axis_label = r"Frequency / {:.3f} Mod. 1".format(deltanu)
  
          lo, hi = np.nanpercentile(ep.value, [0.1, 99.9])
          vlo, vhi = 0.3 * lo, 1.7 * hi
          vstep = (lo - hi) / 500
          color_mapper = LogColorMapper(palette="RdYlGn10", low=lo, high=hi)
  
          fig.image(
              image=[ep.value],
              x=0,
              y=y_f[0].value,
              dw=1,
              dh=y_f[-1].value,
              color_mapper=color_mapper,
              name="img",
          )
  
          stretch_slider = RangeSlider(
              start=vlo,
              end=vhi,
              step=vstep,
              title="",
              value=(lo, hi),
              orientation="vertical",
              width=10,
              height=230,
              direction="rtl",
              show_value=False,
              sizing_mode="fixed",
              name="stretch",
          )
  
          def stretch_change_callback(attr, old, new):
              """TPF stretch slider callback."""
              fig.select("img")[0].glyph.color_mapper.high = new[1]
              fig.select("img")[0].glyph.color_mapper.low = new[0]
  
          stretch_slider.on_change("value", stretch_change_callback)
          return fig, stretch_slider
  
      def interact_echelle(self, notebook_url="localhost:8888", **kwargs):
          """Display an interactive Jupyter notebook widget showing an Echelle diagram.
  
          This feature only works inside an active Jupyter Notebook, and
          requires an optional dependency, ``bokeh`` (v1.0 or later).
          This dependency can be installed using e.g. `conda install bokeh`.
  
          Parameters
          ----------
          notebook_url : str
              Location of the Jupyter notebook page (default: "localhost:8888")
              When showing Bokeh applications, the Bokeh server must be
              explicitly configured to allow connections originating from
              different URLs. This parameter defaults to the standard notebook
              host and port. If you are running on a different location, you
              will need to supply this value for the application to display
              properly. If no protocol is supplied in the URL, e.g. if it is
              of the form "localhost:8888", then "http" will be used.
          """
          try:
              import bokeh
  
              if bokeh.__version__[0] == "0":
                  warnings.warn(
                      "interact() requires Bokeh version 1.0 or later", LightkurveWarning
                  )
          except ImportError:
              log.error(
                  "The interact() tool requires the `bokeh` Python package; "
                  "you can install bokeh using e.g. `conda install bokeh`."
              )
              return None
  
          maximum_frequency = kwargs.pop(
              "maximum_frequency", self.periodogram.frequency.max().value
          )
          minimum_frequency = kwargs.pop(
              "minimum_frequency", self.periodogram.frequency.min().value
          )
  
          if not hasattr(self, "deltanu"):
              dnu = SeismologyQuantity(
                  quantity=self.periodogram.frequency.max() / 30,
                  name="deltanu",
                  method="echelle",
              )
          else:
              dnu = self.deltanu
  
          def create_interact_ui(doc):
              fig_tpf, stretch_slider = self._make_echelle_elements(
                  dnu,
                  maximum_frequency=maximum_frequency,
                  minimum_frequency=minimum_frequency,
                  **kwargs
              )
              maxdnu = self.periodogram.frequency.max().value / 5
              # Interactive slider widgets
              dnu_slider = Slider(
                  start=0.01,
                  end=maxdnu,
                  value=dnu.value,
                  step=0.01,
                  title="Delta Nu",
                  width=290,
              )
              r_button = Button(label=">", button_type="default", width=30)
              l_button = Button(label="<", button_type="default", width=30)
              rr_button = Button(label=">>", button_type="default", width=30)
              ll_button = Button(label="<<", button_type="default", width=30)
  
              def update(attr, old, new):
                  """Callback to take action when dnu slider changes"""
                  dnu = SeismologyQuantity(
                      quantity=dnu_slider.value * u.microhertz,
                      name="deltanu",
                      method="echelle",
                  )
                  ep, _, _ = self._clean_echelle(
                      deltanu=dnu,
                      minimum_frequency=minimum_frequency,
                      maximum_frequency=maximum_frequency,
                      **kwargs
                  )
                  fig_tpf.select("img")[0].data_source.data["image"] = [ep.value]
                  fig_tpf.xaxis.axis_label = r"Frequency / {:.3f} Mod. 1".format(dnu)
  
              def go_right_by_one_small():
                  """Step forward in time by a single cadence"""
                  existing_value = dnu_slider.value
                  if existing_value < maxdnu:
                      dnu_slider.value = existing_value + 0.002
  
              def go_left_by_one_small():
                  """Step back in time by a single cadence"""
                  existing_value = dnu_slider.value
                  if existing_value > 0:
                      dnu_slider.value = existing_value - 0.002
  
              def go_right_by_one():
                  """Step forward in time by a single cadence"""
                  existing_value = dnu_slider.value
                  if existing_value < maxdnu:
                      dnu_slider.value = existing_value + 0.01
  
              def go_left_by_one():
                  """Step back in time by a single cadence"""
                  existing_value = dnu_slider.value
                  if existing_value > 0:
                      dnu_slider.value = existing_value - 0.01
  
              dnu_slider.on_change("value", update)
              r_button.on_click(go_right_by_one_small)
              l_button.on_click(go_left_by_one_small)
              rr_button.on_click(go_right_by_one)
              ll_button.on_click(go_left_by_one)
  
              widgets_and_figures = layout(
                  [fig_tpf, [Spacer(height=20), stretch_slider]],
                  [
                      ll_button,
                      Spacer(width=30),
                      l_button,
                      Spacer(width=25),
                      dnu_slider,
                      Spacer(width=30),
                      r_button,
                      Spacer(width=23),
                      rr_button,
                  ],
              )
              doc.add_root(widgets_and_figures)
  
          output_notebook(verbose=False, hide_banner=True)
          return show(create_interact_ui, notebook_url=notebook_url)
  
      def estimate_numax(self, method="acf2d", **kwargs):
          """Returns the frequency of the peak of the seismic oscillation modes envelope.
  
          At present, the only method supported is based on using a
          2D autocorrelation function (ACF2D).  This method is implemented by the
          `~lightkurve.seismology.estimate_numax_acf2d` function which accepts
          the parameters `numaxs`, `window_width`, and `spacing`.
          For details and literature references, please read the detailed
          docstring of this function by typing ``lightkurve.seismology.estimate_numax_acf2d?``
          in a Python terminal or notebook.
  
          Parameters
          ----------
          method : str
              Method to use. Only ``"acf2d"`` is supported at this time.
  
          Returns
          -------
          numax : `~lightkurve.seismology.SeismologyQuantity`
              Numax of the periodogram, including details on the units and method.
          """
          method = validate_method(method, supported_methods=["acf2d"])
          if method == "acf2d":
              from .numax_estimators import estimate_numax_acf2d
  
              result = estimate_numax_acf2d(self.periodogram, **kwargs)
          self.numax = result
          return result
  
      def diagnose_numax(self, numax=None):
          """Create diagnostic plots showing how numax was estimated."""
          numax = self._validate_numax(numax)
          return numax.diagnostics_plot_method(numax, self.periodogram)
  
      def estimate_deltanu(self, method="acf2d", numax=None):
          """Returns the average value of the large frequency spacing, DeltaNu,
          of the seismic oscillations of the target.
  
          At present, the only method supported is based on using an
          autocorrelation function (ACF2D).  This method is implemented by the
          `~lightkurve.seismology.estimate_deltanu_acf2d` function which requires
          the parameter `numax`. For details and literature references, please
          read the detailed docstring of this function by typing
          ``lightkurve.seismology.estimate_deltanu_acf2d?`` in a Python terminal or notebook.
  
          Parameters
          ----------
          method : str
              Method to use. Only ``"acf2d"`` is supported at this time.
  
          Returns
          -------
          deltanu : `~lightkurve.seismology.SeismologyQuantity`
              DeltaNu of the periodogram, including details on the units and method.
          """
          method = validate_method(method, supported_methods=["acf2d"])
          numax = self._validate_numax(numax)
          if method == "acf2d":
              from .deltanu_estimators import estimate_deltanu_acf2d
  
              result = estimate_deltanu_acf2d(self.periodogram, numax=numax)
          self.deltanu = result
          return result
  
      def diagnose_deltanu(self, deltanu=None):
          """Create diagnostic plots showing how numax was estimated."""
          deltanu = self._validate_deltanu(deltanu)
          return deltanu.diagnostics_plot_method(deltanu, self.periodogram)
  
      def estimate_radius(self, teff=None, numax=None, deltanu=None):
          """Returns a stellar radius estimate based on the scaling relations.
  
          The two global observable seismic parameters, numax and deltanu, along with
          temperature, scale with fundamental stellar properties (Brown et al. 1991;
          Kjeldsen & Bedding 1995). These scaling relations can be rearranged to
          calculate a stellar radius as
  
          R = Rsol * (numax/numax_sol)(deltanu/deltanusol)^-2(Teff/Teffsol)^0.5
  
          where R is the radius and Teff is the effective temperature, and the suffix
          'sol' indicates a solar value. In this method we use the solar values for
          numax and deltanu as given in Huber et al. (2011) and for Teff as given in
          Prsa et al. (2016).
  
          This code structure borrows from work done in Bellinger et al. (2019), which
          also functions as an accessible explanation of seismic scaling relations.
  
          If no value of effective temperature is given, this function will check the
          meta data of the `Periodogram` object used to create the `Seismology` object.
          These data will often contain an effective tempearture from the Kepler Input
          Catalogue (KIC, https://ui.adsabs.harvard.edu/abs/2011AJ....142..112B/abstract),
          or from the EPIC or TIC for K2 and TESS respectively. The temperature values in these
          catalogues are estimated using photometry, and so have large associated uncertainties
          (roughly 200 K, see KIC). For more better results, spectroscopic measurements of
          temperature are often more precise.
  
          NOTE: These scaling relations are scaled to the Sun, and therefore do not
          always produce an entirely accurate result for more evolved stars.
  
          Parameters
          ----------
          numax : float
              The frequency of maximum power of the seismic mode envelope. If not
              given an astropy unit, assumed to be in units of microhertz.
          deltanu : float
              The frequency spacing between two consecutive overtones of equal radial
              degree. If not given an astropy unit, assumed to be in units of
              microhertz.
          teff : float
              The effective temperature of the star. In units of Kelvin.
          numax_err : float
              Error on numax. Assumed to be same units as numax
          deltanu_err : float
              Error on deltanu. Assumed to be same units as deltanu
          teff_err : float
              Error on Teff. Assumed to be same units as Teff.
  
          Returns
          -------
          radius : `~lightkurve.seismology.SeismologyQuantity`
              Stellar radius estimate.
          """
          numax = self._validate_numax(numax)
          deltanu = self._validate_deltanu(deltanu)
          if teff is None:
              teff = self.periodogram.meta.get("TEFF")
              if teff is None:
                  raise ValueError(
                      "You must provide an effective temperature argument (`teff`) to `estimate_radius`,"
                      "because the Periodogram object does not contain it in its meta data (i.e. `pg.meta['TEFF']` is missing"
                  )
              else:
                  log.info(
                      "Using value for effective temperature from the Kepler Input Catalogue."
                      "These temperatue values may sometimes differ significantly from modern estimates."
                  )
                  pass
          else:
              pass
  
          result = stellar_estimators.estimate_radius(numax, deltanu, teff)
          self.radius = result
          return result
  
      def estimate_mass(self, teff=None, numax=None, deltanu=None):
          """Calculates mass using the asteroseismic scaling relations.
  
          The two global observable seismic parameters, numax and deltanu, along with
          temperature, scale with fundamental stellar properties (Brown et al. 1991;
          Kjeldsen & Bedding 1995). These scaling relations can be rearranged to
          calculate a stellar mass as
  
          M = Msol * (numax/numax_sol)^3(deltanu/deltanusol)^-4(Teff/Teffsol)^1.5
  
          where M is the mass and Teff is the effective temperature, and the suffix
          'sol' indicates a solar value. In this method we use the solar values for
          numax and deltanu as given in Huber et al. (2011) and for Teff as given in
          Prsa et al. (2016).
  
          This code structure borrows from work done in Bellinger et al. (2019), which
          also functions as an accessible explanation of seismic scaling relations.
  
          If no value of effective temperature is given, this function will check the
          meta data of the `Periodogram` object used to create the `Seismology` object.
          These data will often contain an effective tempearture from the Kepler Input
          Catalogue (KIC, https://ui.adsabs.harvard.edu/abs/2011AJ....142..112B/abstract),
          or from the EPIC or TIC for K2 and TESS respectively. The temperature values in these
          catalogues are estimated using photometry, and so have large associated uncertainties
          (roughly 200 K, see KIC). For more better results, spectroscopic measurements of
          temperature are often more precise.
  
          NOTE: These scaling relations are scaled to the Sun, and therefore do not
          always produce an entirely accurate result for more evolved stars.
  
          Parameters
          ----------
          numax : float
              The frequency of maximum power of the seismic mode envelope. If not
              given an astropy unit, assumed to be in units of microhertz.
          deltanu : float
              The frequency spacing between two consecutive overtones of equal radial
              degree. If not given an astropy unit, assumed to be in units of
              microhertz.
          teff : float
              The effective temperature of the star. In units of Kelvin.
          numax_err : float
              Error on numax. Assumed to be same units as numax
          deltanu_err : float
              Error on deltanu. Assumed to be same units as deltanu
          teff_err : float
              Error on Teff. Assumed to be same units as Teff.
  
          Returns
          -------
          mass : `~lightkurve.seismology.SeismologyQuantity`
              Stellar mass estimate.
          """
          numax = self._validate_numax(numax)
          deltanu = self._validate_deltanu(deltanu)
          if teff is None:
              teff = self.periodogram.meta.get("TEFF")
              if teff is None:
                  raise ValueError(
                      "You must provide an effective temperature argument (`teff`) to `estimate_radius`,"
                      "because the Periodogram object does not contain it in its meta data (i.e. `pg.meta['TEFF']` is missing"
                  )
              else:
                  log.info(
                      "Using value for effective temperature from the Kepler Input Catalogue."
                      "These temperatue values may sometimes differ significantly from modern estimates."
                  )
                  pass
          else:
              pass
  
          result = stellar_estimators.estimate_mass(numax, deltanu, teff)
          self.mass = result
          return result
  
      def estimate_logg(self, teff=None, numax=None):
          """Calculates the log of the surface gravity using the asteroseismic scaling
          relations.
  
          The two global observable seismic parameters, numax and deltanu, along with
          temperature, scale with fundamental stellar properties (Brown et al. 1991;
          Kjeldsen & Bedding 1995). These scaling relations can be rearranged to
          calculate a stellar surface gravity as
  
              g = gsol * (numax/numax_sol)(Teff/Teffsol)^0.5
  
          where g is the surface gravity and Teff is the effective temperature,
          and the suffix 'sol' indicates a solar value. In this method we use the
          solar values for numax as given in Huber et al. (2011) and for Teff as given
          in Prsa et al. (2016). The solar surface gravity is calcluated from the
          astropy constants for solar mass and radius and does not have an error.
  
          The solar surface gravity is returned as log10(g) with units in dex, as is
          common in the astrophysics literature.
  
          This code structure borrows from work done in Bellinger et al. (2019), which
          also functions as an accessible explanation of seismic scaling relations.
  
          If no value of effective temperature is given, this function will check the
          meta data of the `Periodogram` object used to create the `Seismology` object.
          These data will often contain an effective tempearture from the Kepler Input
          Catalogue (KIC, https://ui.adsabs.harvard.edu/abs/2011AJ....142..112B/abstract),
          or from the EPIC or TIC for K2 and TESS respectively. The temperature values in these
          catalogues are estimated using photometry, and so have large associated uncertainties
          (roughly 200 K, see KIC). For more better results, spectroscopic measurements of
          temperature are often more precise.
  
          NOTE: These scaling relations are scaled to the Sun, and therefore do not
          always produce an entirely accurate result for more evolved stars.
  
          Parameters
          ----------
          numax : float
              The frequency of maximum power of the seismic mode envelope. If not
              given an astropy unit, assumed to be in units of microhertz.
          teff : float
              The effective temperature of the star. In units of Kelvin.
          numax_err : float
              Error on numax. Assumed to be same units as numax
          teff_err : float
              Error on teff. Assumed to be same units as teff.
  
          Returns
          -------
          logg : `~lightkurve.seismology.SeismologyQuantity`
              Stellar surface gravity estimate.
          """
          numax = self._validate_numax(numax)
          if teff is None:
              teff = self.periodogram.meta.get("TEFF")
              if teff is None:
                  raise ValueError(
                      "You must provide an effective temperature argument (`teff`) to `estimate_radius`,"
                      "because the Periodogram object does not contain it in its meta data (i.e. `pg.meta['TEFF']` is missing"
                  )
              else:
                  log.info(
                      "Using value for effective temperature from the Kepler Input Catalogue."
                      "These temperatue values may sometimes differ significantly from modern estimates."
                  )
                  pass
          else:
              pass
          result = stellar_estimators.estimate_logg(numax, teff)
          self.logg = result
          return result
  