Skip to content

Commit

Permalink
OwTSNE: Don't clear computed values at invalidation, set flags instead
Browse files Browse the repository at this point in the history
  • Loading branch information
pavlin-policar committed May 6, 2019
1 parent 914f3cd commit 7c1dcea
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 9 deletions.
56 changes: 47 additions & 9 deletions Orange/widgets/unsupervised/owtsne.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,24 @@ def update_coordinates(self):
self.view_box.setAspectLocked(True, 1)


class invalidated:
pca_projection = affinities = tsne_embedding = False

def __set__(self, instance, value):
# `self._invalidate = True` should invalidate everything
self.pca_projection = self.affinities = self.tsne_embedding = value

def __bool__(self):
# If any of the values are invalidated, this should return true
return self.pca_projection or self.affinities or self.tsne_embedding

def __str__(self):
return "%s(%s)" % (self.__class__.__name__, ", ".join(
"=".join([k, str(getattr(self, k))])
for k in ["pca_projection", "affinities", "tsne_embedding"]
))


class OWtSNE(OWDataProjectionWidget, ConcurrentWidgetMixin):
name = "t-SNE"
description = "Two-dimensional data projection with t-SNE."
Expand All @@ -250,6 +268,11 @@ class OWtSNE(OWDataProjectionWidget, ConcurrentWidgetMixin):

left_side_scrolling = True

# Use `invalidated` descriptor so we don't break the usage of
# `_invalidated` in `OWDataProjectionWidget`, but still allow finer control
# over which parts of the embedding to invalidate
_invalidated = invalidated()

class Information(OWDataProjectionWidget.Information):
modified = Msg("The parameter settings have been changed. Press "
"\"Start\" to rerun with the new settings.")
Expand Down Expand Up @@ -323,21 +346,19 @@ def _multiscale_changed(self):
self._invalidate_affinities()

def _invalidate_pca_projection(self):
self.pca_projection = None
self.initialization = None
self._invalidated.pca_projection = True
self._invalidate_affinities()

def _invalidate_affinities(self):
self.affinities = None
self._invalidated.affinities = True
self._invalidate_tsne_embedding()

def _invalidate_tsne_embedding(self):
self.iterations_done = 0
self.tsne_embedding = None
self._invalidate_output()
self._invalidated.tsne_embedding = True
self._stop_running_task()
self._set_modified(True)

def _invalidate_output(self):
def _stop_running_task(self):
self.cancel()
self.run_button.setText("Start")

Expand Down Expand Up @@ -397,8 +418,15 @@ def _toggle_run(self):
else:
self.run()

def set_data(self, data: Table):
super().set_data(data)
def handleNewSignals(self):
# We don't bother with the granular invalidation flags because
# `super().handleNewSignals` will just set all of them to False or will
# do nothing. However, it's important we remember its state because we
# won't call `run` if needed. `run` also relies on the state of
# `_invalidated` to properly set the intermediate values to None
prev_invalidated = bool(self._invalidated)
super().handleNewSignals()
self._invalidated = prev_invalidated

if self._invalidated:
self.run()
Expand Down Expand Up @@ -439,7 +467,17 @@ def enable_controls(self):
self.controls.perplexity.setDisabled(self.multiscale)

def run(self):
# Reset invalidated values as indicated by the flags
if self._invalidated.pca_projection:
self.pca_projection = None
if self._invalidated.affinities:
self.affinities = None
if self._invalidated.tsne_embedding:
self.iterations_done = 0
self.tsne_embedding = None

self._set_modified(False)
self._invalidated = False

# When the data is invalid, it is set to `None` and an error is set,
# therefore it would be erroneous to clear the error here
Expand Down
52 changes: 52 additions & 0 deletions Orange/widgets/unsupervised/tests/test_owtsne.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,58 @@ def test_global_structure_info_msg_persists_if_data_is_reloaded(self):
"The information message was cleared after data was reloaded"
)

def test_invalidation_flow(self):
w = self.widget
# Setup widget: send data to input with global structure "off", then
# set global structure "on" (after the embedding is computed)
w.controls.multiscale.setChecked(False)
self.send_signal(w.Inputs.data, self.data)
self.wait_until_stop_blocking()
self.assertFalse(self.widget.Information.modified.is_shown())
# All the embedding components should computed
self.assertIsNotNone(w.pca_projection)
self.assertIsNotNone(w.affinities)
self.assertIsNotNone(w.tsne_embedding)
# All the invalidation flags should be set to false
self.assertFalse(w._invalidated.pca_projection)
self.assertFalse(w._invalidated.affinities)
self.assertFalse(w._invalidated.tsne_embedding)

# Trigger invalidation
w.controls.multiscale.setChecked(True)
self.assertTrue(self.widget.Information.modified.is_shown())
# Setting `multiscale` to true should set the invalidate flags for
# the affinities and embedding, but not the pca_projection
self.assertFalse(w._invalidated.pca_projection)
self.assertTrue(w._invalidated.affinities)
self.assertTrue(w._invalidated.tsne_embedding)

# The flags should now be set, but the embedding should still be
# available when selecting a subset of data and such
self.assertIsNotNone(w.pca_projection)
self.assertIsNotNone(w.affinities)
self.assertIsNotNone(w.tsne_embedding)

# We should still be able to send a data subset to the input and have
# the points be highlighted
self.send_signal(w.Inputs.data_subset, self.data[:10])
self.wait_until_stop_blocking()
subset = [brush.color().name() == "#46befa" for brush in
w.graph.scatterplot_item.data["brush"][:10]]
other = [brush.color().name() == "#000000" for brush in
w.graph.scatterplot_item.data["brush"][10:]]
self.assertTrue(all(subset))
self.assertTrue(all(other))

# Clear the data subset
self.send_signal(w.Inputs.data_subset, None)

# Run the optimization
self.widget.run_button.clicked.emit()
self.wait_until_stop_blocking()
# All of the inavalidation flags should have been cleared
self.assertFalse(w._invalidated)


class TestTSNERunner(unittest.TestCase):
@classmethod
Expand Down

0 comments on commit 7c1dcea

Please sign in to comment.