Skip to content

Commit

Permalink
Add errors for no fit provided, all points masked
Browse files Browse the repository at this point in the history
Previously, if an image came in with all points masked the interactive
mode would fail without a useful message. It now catches two new error
cases:

+ If an image is empty/completely masked when passed to an
  InteractiveModel1D.
+ If a fit cannot be evaluated, including specific instructions to check
  if an initial fit should be present or if there is anything wrong with
  the image
  • Loading branch information
teald committed Feb 15, 2024
1 parent 8cef238 commit 385e3c0
Showing 1 changed file with 56 additions and 27 deletions.
83 changes: 56 additions & 27 deletions geminidr/interactive/fit/fit1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,22 @@


# Names to use for masks. You can change these to change the label that gets displayed in the legend
SIGMA_MASK_NAME = 'rejected (sigma)'
USER_MASK_NAME = 'rejected (user)'
BAND_MASK_NAME = 'excluded'
INPUT_MASK_NAMES = ['aperture', 'threshold']
SIGMA_MASK_NAME = "rejected (sigma)"
USER_MASK_NAME = "rejected (user)"
BAND_MASK_NAME = "excluded"
INPUT_MASK_NAMES = ["aperture", "threshold"]


class InteractiveModelError(Exception):
"""Base class for exceptions in this module."""


class InteractiveModel(ABC):
"""Base class for all interactive models, containing:
(a) the parameters of the model
(b) the parameters that control the fitting (e.g., sigma-clipping)
(c) the way the fitting is performed
(d) the input and output coordinates, mask, and weights
(a) the parameters of the model
(b) the parameters that control the fitting (e.g., sigma-clipping)
(c) the way the fitting is performed
(d) the input and output coordinates, mask, and weights
"""

MASK_TYPE = [
Expand Down Expand Up @@ -180,6 +184,7 @@ def __init__(
extra_masks : dict of boolean arrays
points to display but not use in fit
initial_fit: fit_1D/None
an initial fit to use if there are no data points
"""
Expand All @@ -197,15 +202,15 @@ def __init__(
self.fitting_parameters = fitting_parameters
self.domain = domain
self.fit = initial_fit
if len(x) == 0:
self.fit = initial_fit

self.listeners = listeners

self.section = section
self.data = bm.ColumnDataSource({"x": [], "y": [], "mask": []})

if self.domain:
xlinspace = np.linspace(*self.domain, 500)

else:
xlinspace = np.linspace(min(x), max(x), 500)

Expand Down Expand Up @@ -299,8 +304,15 @@ def populate_bokeh_objects(
else:
init_mask = mask

# Check if the data has been fully masked; if so, raise a ValueError.
if init_mask.all():
raise ValueError(
"All data points are masked. Cannot perform a fit."
)

x = x[~init_mask]
y = y[~init_mask]

if weights is not None:
weights = weights[~init_mask]

Expand Down Expand Up @@ -465,6 +477,7 @@ def perform_fit(self, *args):
)

self.quality = FitQuality.BAD

if goodpix.sum():
new_fit = fit_1D(
self.y[goodpix],
Expand All @@ -480,25 +493,32 @@ def perform_fit(self, *args):
# Chebyshevs it's effectively the number of fitted points (max
# order+1).
rank = new_fit.fit_info["rank"]

if rank > 0:
if "params" in new_fit.fit_info: # it's a polynomial
rank -= 1

if rank >= fitparms["order"]:
self.quality = FitQuality.GOOD
self.fit = new_fit

elif self.fit is None:
self.quality = FitQuality.BAD
self.fit = new_fit

else:
# Modify the fit_1D object with a shift by ugly hacking
offset = np.mean(self.y[goodpix] - self.evaluate(self.x[goodpix]))
offset = np.mean(
self.y[goodpix] - self.evaluate(self.x[goodpix])
)
self.fit.offset_fit(offset)
self.fit.points = new_fit.points
self.fit.mask = new_fit.mask
self.quality = FitQuality.POOR # else stay BAD

if self.quality != FitQuality.BAD: # don't update if it's BAD
self.fit = new_fit

if "residuals" in self.data.data:
self.data.data["residuals"] = self.y - self.evaluate(self.x)

Expand Down Expand Up @@ -532,13 +552,25 @@ def update_mask(self):
self.data.data["mask"] = mask

def evaluate(self, x):
return self.fit.evaluate(x)
try:
return self.fit.evaluate(x)

except AttributeError as err:
msg = f"Could not evaluate fit ({self.fit = })."

if self.fit is None:
msg += " Have you provided an initial fit?"

msg += " Is the image empty or completely masked?"

raise InteractiveModelError(msg) from err


class FittingParametersUI:
"""Manager for the UI controls and their interactions with the fitting
model.
"""

def __init__(self, vis, fit, fitting_parameters):
"""Class to manage the set of UI controls for the inputs to the fitting
model.
Expand Down Expand Up @@ -967,6 +999,7 @@ class Fit1DPanel:
This class is typically used in tabs within the interactive module. It
is meant to handle one set of data being fit at a time.
"""

def __init__(
self,
visualizer,
Expand Down Expand Up @@ -1154,7 +1187,7 @@ def __init__(
css_classes=["tab-content"],
spacing=10,
stylesheets=dragons_styles(),
sizing_mode="stretch_width"
sizing_mode="stretch_width",
)

# pylint: disable=unused-argument
Expand Down Expand Up @@ -1449,8 +1482,7 @@ def _point_mask_handler(self, x, y, mult, action):
# x/y tracking when the mouse moves in the figure for calculateSensitivity
@staticmethod
def add_custom_cursor_behavior(pointer):
"""Customize cursor behavior depending on which tool is active.
"""
"""Customize cursor behavior depending on which tool is active."""
pan_start = """
var mainPlot = document.getElementsByClassName('plot-main')[0];
var active = [...mainPlot.getElementsByClassName('bk-active')];
Expand Down Expand Up @@ -1651,7 +1683,7 @@ def __init__(
self.layout = None
self.recalc_inputs_above = recalc_inputs_above

if 'pad_buttons' in kwargs:
if "pad_buttons" in kwargs:
# Deprecation warning
warnings.warn(
"pad_buttons is no longer supported",
Expand Down Expand Up @@ -1716,14 +1748,12 @@ def kickoff_modal(attr, old, new):

if recalc_inputs_above:
self.reinit_panel = row(
*reinit_widgets,
stylesheets=dragons_styles()
*reinit_widgets, stylesheets=dragons_styles()
)

else:
self.reinit_panel = column(
*reinit_widgets,
stylesheets=dragons_styles()
*reinit_widgets, stylesheets=dragons_styles()
)

else:
Expand Down Expand Up @@ -1838,7 +1868,7 @@ def visualize(self, doc):
col = column(
self.tabs,
stylesheets=dragons_styles(),
sizing_mode="stretch_width"
sizing_mode="stretch_width",
)

for btn in (self.submit_button, self.abort_button):
Expand Down Expand Up @@ -1907,14 +1937,14 @@ def visualize(self, doc):
self.reinit_panel,
col,
sizing_mode="stretch_width",
stylesheets=dragons_styles()
stylesheets=dragons_styles(),
)
)

self.layout = column(
*layout_ls,
sizing_mode="stretch_width",
stylesheets=dragons_styles()
stylesheets=dragons_styles(),
)

doc.add_root(self.layout)
Expand Down Expand Up @@ -1959,6 +1989,7 @@ def function():
self.do_later(function)

if self.reconstruct_points_fn is not None:

def rfn():
data = None
try:
Expand All @@ -1977,7 +2008,7 @@ def rfn():
logging.error(
"Unable to build data from inputs, got Exception %s",
err,
exc_info=True
exc_info=True,
)

if data is not None:
Expand Down Expand Up @@ -2211,9 +2242,7 @@ def fit1d_figure(

if plot_residuals and plot_ratios:
tabs = bm.Tabs(
tabs=[],
sizing_mode="stretch_width",
stylesheets=dragons_styles()
tabs=[], sizing_mode="stretch_width", stylesheets=dragons_styles()
)

tabs.tabs.append(bm.TabPanel(child=p_resid, title="Residuals"))
Expand Down

0 comments on commit 385e3c0

Please sign in to comment.