Skip to content

Commit

Permalink
Merge pull request #1635 from astrofrog/new-spectrum-viewer
Browse files Browse the repository at this point in the history
Refactor spectrum tool
  • Loading branch information
astrofrog authored Apr 3, 2018
2 parents cb92601 + be5f3cb commit 3fd167a
Show file tree
Hide file tree
Showing 39 changed files with 2,632 additions and 2,127 deletions.
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Full changelog
v0.13.0 (unreleased)
--------------------

* Re-write spectrum viewer into a generic profile viewer that uses
subsets to define the areas in which to compute profiles rather
than custom ROIs. [#1635]

* Improve performance when changing visual attributes of subsets.
[#1617]

Expand Down
2 changes: 1 addition & 1 deletion glue/core/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def axis_label(self, axis):
return "World {}".format(axis)

def dependent_axes(self, axis):
"""Return a tuple of which world-axes are non-indepndent
"""Return a tuple of which world-axes are non-independent
from a given pixel axis
The axis index is given in numpy ordering convention (note that
Expand Down
6 changes: 3 additions & 3 deletions glue/core/fitters.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__(self, **params):
else:
setattr(self, k, v)

def plot(self, fit_result, axes, x):
def plot(self, fit_result, axes, x, linewidth=None, alpha=None, color=None):
"""
Plot the result of a fit.
Expand All @@ -54,8 +54,8 @@ def plot(self, fit_result, axes, x):
plots will not be properly cleared if this isn't provided
"""
y = self.predict(fit_result, x)
result = axes.plot(x, y, '#4daf4a',
lw=3, alpha=0.8,
result = axes.plot(x, y, color,
lw=linewidth, alpha=alpha,
scalex=False, scaley=False)
return result

Expand Down
43 changes: 42 additions & 1 deletion glue/core/link_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from glue.core.data import Data
from glue.core.component import DerivedComponent
from glue.core.exceptions import IncompatibleAttribute

from glue.core.subset import Subset

__all__ = ['accessible_links', 'discover_links', 'find_dependents',
'LinkManager', 'is_equivalent_cid']
Expand Down Expand Up @@ -306,3 +306,44 @@ def is_equivalent_cid(data, cid1, cid2):
cid2 = _find_identical_reference_cid(data, cid2)

return cid1 is cid2


def is_convertible_to_single_pixel_cid(data, cid):
"""
Given a dataset and a component ID, determine whether a pixel component
exists in data such that the component ID can be derived solely from the
pixel component. Returns `None` if no such pixel component ID can be found
and returns the pixel component ID if one exists.
Parameters
----------
data : `~glue.core.Data`
The data in which to check for pixel components IDs
cid : `~glue.core.ComponentID`
The component ID to search for
"""
if isinstance(data, Subset):
data = data.data
if cid in data.pixel_component_ids:
return cid
else:
try:
target_comp = data.get_component(cid)
except IncompatibleAttribute:
return None
if cid in data.world_component_ids:
if len(data.coords.dependent_axes(target_comp.axis)) == 1:
return data.pixel_component_ids[target_comp.axis]
else:
return None
else:
if isinstance(target_comp, DerivedComponent):
from_ids = [is_convertible_to_single_pixel_cid(data, cid)
for cid in target_comp.link.get_from_ids()]
if None in from_ids:
return None
else:
# Use set to get rid of duplicates
from_ids = list(set(from_ids))
if len(from_ids) == 1:
return is_convertible_to_single_pixel_cid(data, from_ids[0])
158 changes: 158 additions & 0 deletions glue/core/qt/fitters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from qtpy import QtWidgets, QtGui

from glue.core.qt.simpleforms import build_form_item

__all__ = ['ConstraintsWidget', 'FitSettingsWidget']


class ConstraintsWidget(QtWidgets.QWidget):

"""
A widget to display and tweak the constraints of a :class:`~glue.core.fitters.BaseFitter1D`
"""

def __init__(self, constraints, parent=None):
"""
Parameters
----------
constraints : dict
The `contstraints` property of a :class:`~glue.core.fitters.BaseFitter1D`
object
parent : QtWidgets.QWidget (optional)
The parent of this widget
"""
super(ConstraintsWidget, self).__init__(parent)
self.constraints = constraints

self.layout = QtWidgets.QGridLayout()
self.layout.setContentsMargins(2, 2, 2, 2)
self.layout.setSpacing(4)

self.setLayout(self.layout)

self.layout.addWidget(QtWidgets.QLabel("Estimate"), 0, 1)
self.layout.addWidget(QtWidgets.QLabel("Fixed"), 0, 2)
self.layout.addWidget(QtWidgets.QLabel("Bounded"), 0, 3)
self.layout.addWidget(QtWidgets.QLabel("Lower Bound"), 0, 4)
self.layout.addWidget(QtWidgets.QLabel("Upper Bound"), 0, 5)

self._widgets = {}
names = sorted(list(self.constraints.keys()))

for k in names:
row = []
w = QtWidgets.QLabel(k)
row.append(w)

v = QtGui.QDoubleValidator()
e = QtWidgets.QLineEdit()
e.setValidator(v)
e.setText(str(constraints[k]['value'] or ''))
row.append(e)

w = QtWidgets.QCheckBox()
w.setChecked(constraints[k]['fixed'])
fix = w
row.append(w)

w = QtWidgets.QCheckBox()
limits = constraints[k]['limits']
w.setChecked(limits is not None)
bound = w
row.append(w)

e = QtWidgets.QLineEdit()
e.setValidator(v)
if limits is not None:
e.setText(str(limits[0]))
row.append(e)

e = QtWidgets.QLineEdit()
e.setValidator(v)
if limits is not None:
e.setText(str(limits[1]))
row.append(e)

def unset(w):
def result(active):
if active:
w.setChecked(False)
return result

fix.toggled.connect(unset(bound))
bound.toggled.connect(unset(fix))

self._widgets[k] = row

for i, row in enumerate(names, 1):
for j, widget in enumerate(self._widgets[row]):
self.layout.addWidget(widget, i, j)

def settings(self, name):
""" Return the constraints for a single model parameter """
row = self._widgets[name]
name, value, fixed, limited, lo, hi = row
value = float(value.text()) if value.text() else None
fixed = fixed.isChecked()
limited = limited.isChecked()
lo = lo.text()
hi = hi.text()
limited = limited and not ((not lo) or (not hi))
limits = None if not limited else [float(lo), float(hi)]
return dict(value=value, fixed=fixed, limits=limits)

def update_constraints(self, fitter):
""" Update the constraints in a :class:`~glue.core.fitters.BaseFitter1D`
based on the settings in this widget
"""
for name in self._widgets:
s = self.settings(name)
fitter.set_constraint(name, **s)


class FitSettingsWidget(QtWidgets.QDialog):

def __init__(self, fitter, parent=None):
super(FitSettingsWidget, self).__init__(parent)
self.fitter = fitter

self._build_form()
self._connect()
self.setModal(True)

def _build_form(self):
fitter = self.fitter

l = QtWidgets.QFormLayout()
options = fitter.options
self.widgets = {}
self.forms = {}

for k in sorted(options):
item = build_form_item(fitter, k)
l.addRow(item.label, item.widget)
self.widgets[k] = item.widget
self.forms[k] = item # need to prevent garbage collection

constraints = fitter.constraints
if constraints:
self.constraints = ConstraintsWidget(constraints)
l.addRow(self.constraints)
else:
self.constraints = None

self.okcancel = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok |
QtWidgets.QDialogButtonBox.Cancel)
l.addRow(self.okcancel)
self.setLayout(l)

def _connect(self):
self.okcancel.accepted.connect(self.accept)
self.okcancel.rejected.connect(self.reject)
self.accepted.connect(self.update_fitter_from_settings)

def update_fitter_from_settings(self):
for k, v in self.widgets.items():
setattr(self.fitter, k, v.value())
if self.constraints is not None:
self.constraints.update_constraints(self.fitter)
46 changes: 46 additions & 0 deletions glue/core/qt/tests/test_fitters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from mock import MagicMock

from glue.core.fitters import SimpleAstropyGaussianFitter, PolynomialFitter

from ..fitters import ConstraintsWidget, FitSettingsWidget


class TestConstraintsWidget(object):

def setup_method(self, method):
self.constraints = dict(a=dict(fixed=True, value=1, limits=None))
self.widget = ConstraintsWidget(self.constraints)

def test_settings(self):
assert self.widget.settings('a') == dict(fixed=True, value=1,
limits=None)

def test_update_settings(self):
self.widget._widgets['a'][2].setChecked(False)
assert self.widget.settings('a')['fixed'] is False

def test_update_constraints(self):
self.widget._widgets['a'][2].setChecked(False)
fitter = MagicMock()
self.widget.update_constraints(fitter)
fitter.set_constraint.assert_called_once_with('a',
fixed=False, value=1,
limits=None)


class TestFitSettingsWidget(object):

def test_option(self):
f = PolynomialFitter()
f.degree = 1
w = FitSettingsWidget(f)
w.widgets['degree'].setValue(5)
w.update_fitter_from_settings()
assert f.degree == 5

def test_set_constraints(self):
f = SimpleAstropyGaussianFitter()
w = FitSettingsWidget(f)
w.constraints._widgets['amplitude'][2].setChecked(True)
w.update_fitter_from_settings()
assert f.constraints['amplitude']['fixed']
8 changes: 8 additions & 0 deletions glue/core/state_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from glue.external.echo import (delay_callback, CallbackProperty,
HasCallbackProperties, CallbackList)
from glue.core.state import saver, loader
from glue.core.component_id import PixelComponentID

__all__ = ['State', 'StateAttributeCacheHelper',
'StateAttributeLimitsHelper', 'StateAttributeSingleValueHelper']
Expand Down Expand Up @@ -322,6 +323,13 @@ def update_values(self, force=False, use_default_modifiers=False, **properties):

else:

# Shortcut if the component ID is a pixel component ID
if isinstance(self.component_id, PixelComponentID) and percentile == 100 and not log:
lower = -0.5
upper = self.data.shape[self.component_id.axis] - 0.5
self.set(lower=lower, upper=upper, percentile=percentile, log=log)
return

exclude = (100 - percentile) / 2.

data_values = self.data_values
Expand Down
64 changes: 61 additions & 3 deletions glue/core/tests/test_link_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
import numpy as np

from ..component_link import ComponentLink
from ..data import ComponentID, DerivedComponent
from ..data import Data, Component
from ..data import ComponentID, DerivedComponent, Data, Component
from ..coordinates import Coordinates
from ..data_collection import DataCollection
from ..link_manager import (LinkManager, accessible_links, discover_links,
find_dependents)
find_dependents, is_convertible_to_single_pixel_cid)
from ..link_helpers import LinkSame

comp = Component(data=np.array([1, 2, 3]))
Expand Down Expand Up @@ -300,3 +300,61 @@ def test_remove_component_removes_links(self):

assert len(d1.externally_derivable_components) == 0
assert len(d2.externally_derivable_components) == 0


def test_is_convertible_to_single_pixel_cid():

# This tests the function is_convertible_to_single_pixel_cid, which gives
# for a given dataset the pixel component ID that can be uniquely
# transformed into the requested component ID.

# Set up a coordinate object which has an independent first axis and
# has the second and third axes depend on each other. The transformation
# itself is irrelevant since for this function to work we only care about
# whether or not an axis is independent.

class CustomCoordinates(Coordinates):
def dependent_axes(self, axis):
if axis == 0:
return (0,)
else:
return (1, 2)

data1 = Data()
data1.coords = CustomCoordinates()
data1['x'] = np.ones((4, 3, 4))
px1, py1, pz1 = data1.pixel_component_ids
wx1, wy1, wz1 = data1.world_component_ids
data1['a'] = px1 * 2
data1['b'] = wx1 * 2
data1['c'] = wy1 * 2
data1['d'] = wx1 * 2 + px1
data1['e'] = wx1 * 2 + wy1

# Pixel component IDs should just be returned directly
for cid in data1.pixel_component_ids:
assert is_convertible_to_single_pixel_cid(data1, cid) is cid

# Only the first world component should return a valid pixel component
# ID since the two other world components are interlinked
assert is_convertible_to_single_pixel_cid(data1, wx1) is px1
assert is_convertible_to_single_pixel_cid(data1, wy1) is None
assert is_convertible_to_single_pixel_cid(data1, wz1) is None

# a and b are ultimately linked to the first pixel coordinate, whereas c
# depends on the second world coordinate which is interlinked with the third
# Finally, d is ok because it really only depends on px1
assert is_convertible_to_single_pixel_cid(data1, data1.id['a']) is px1
assert is_convertible_to_single_pixel_cid(data1, data1.id['b']) is px1
assert is_convertible_to_single_pixel_cid(data1, data1.id['c']) is None
assert is_convertible_to_single_pixel_cid(data1, data1.id['d']) is px1
assert is_convertible_to_single_pixel_cid(data1, data1.id['e']) is None

# We now create a second dataset and set up links
data2 = Data(y=np.ones((4, 5, 6, 7)), z=np.zeros((4, 5, 6, 7)))
dc = DataCollection([data1, data2])
dc.add_link(ComponentLink([data1.id['a'], px1], data2.id['y'], using=lambda x: 2 * x))
dc.add_link(ComponentLink([wy1], data2.id['z'], using=lambda x: 2 * x))

assert is_convertible_to_single_pixel_cid(data1, data2.id['y']) is px1
assert is_convertible_to_single_pixel_cid(data1, data2.id['z']) is None
Loading

0 comments on commit 3fd167a

Please sign in to comment.