Skip to content

Commit d27978a

Browse files
committed
OWConcurrentProjectionWidget: Add an example
1 parent 049b6ed commit d27978a

File tree

3 files changed

+201
-1
lines changed

3 files changed

+201
-1
lines changed

Orange/widgets/tests/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,10 +857,13 @@ def _compare_selected_annotated_domains(self, selected, annotated):
857857
annotated_vars = annotated.domain.variables
858858
self.assertLessEqual(set(selected_vars), set(annotated_vars))
859859

860-
def test_setup_graph(self):
860+
def test_setup_graph(self, timeout=DEFAULT_TIMEOUT):
861861
"""Plot should exist after data has been sent in order to be
862862
properly set/updated"""
863863
self.send_signal(self.widget.Inputs.data, self.data)
864+
if self.widget.isBlocking():
865+
spy = QSignalSpy(self.widget.blockingStateChanged)
866+
self.assertTrue(spy.wait(timeout))
864867
self.assertIsNotNone(self.widget.graph.scatterplot_item)
865868

866869
def test_default_attrs(self, timeout=DEFAULT_TIMEOUT):
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# pylint: disable=too-many-ancestors
2+
from typing import Optional
3+
from types import SimpleNamespace as namespace
4+
5+
import numpy as np
6+
7+
from Orange.data import Table
8+
from Orange.widgets import gui
9+
from Orange.widgets.settings import Setting
10+
from Orange.widgets.utils.concurrent import TaskState, ConcurrentWidgetMixin
11+
from Orange.widgets.utils.widgetpreview import WidgetPreview
12+
from Orange.widgets.visualize.utils.widget import OWDataProjectionWidget
13+
14+
15+
class Result(namespace):
16+
embedding = None # type: Optional[np.ndarray]
17+
18+
19+
def run(data: Table, embedding: Optional[np.ndarray], state: TaskState):
20+
res = Result(embedding=embedding)
21+
22+
# simulate wasteful calculation (increase 'steps')
23+
step, steps = 0, 10
24+
state.set_status("Calculating...")
25+
while step < steps:
26+
for _ in range(steps):
27+
x_data = np.array(np.mean(data.X, axis=1))
28+
if x_data.ndim == 2:
29+
x_data = x_data.ravel()
30+
y_data = np.random.rand(len(x_data))
31+
# Needs a copy because projection should not be modified
32+
# inplace. If it is modified inplace, the widget and the thread
33+
# hold a reference to the same object. When the thread is
34+
# interrupted it is still modifying the object, but the widget
35+
# receives it (the modified object) with a delay.
36+
embedding = np.vstack((x_data, y_data)).T.copy()
37+
step += 1
38+
if step % (steps / 10) == 0:
39+
state.set_progress_value(100 * step / steps)
40+
41+
if state.is_interruption_requested():
42+
return res
43+
44+
res.embedding = embedding
45+
state.set_partial_result(res)
46+
return res
47+
48+
49+
class OWConcurrentWidget(OWDataProjectionWidget, ConcurrentWidgetMixin):
50+
name = "Projection"
51+
param = Setting(0)
52+
53+
def __init__(self):
54+
OWDataProjectionWidget.__init__(self)
55+
ConcurrentWidgetMixin.__init__(self)
56+
self.embedding = None # type: Optional[np.ndarray]
57+
58+
# GUI
59+
def _add_controls(self):
60+
box = gui.vBox(self.controlArea, True)
61+
gui.comboBox(
62+
box, self, "param", label="Parameter:",
63+
items=["Param A", "Param B"], labelWidth=80,
64+
callback=self.__param_combo_changed
65+
)
66+
self.run_button = gui.button(box, self, "Start", self._toggle_run)
67+
super()._add_controls()
68+
69+
def __param_combo_changed(self):
70+
self._run()
71+
72+
def _toggle_run(self):
73+
if self.data is None:
74+
return
75+
76+
# Pause task
77+
if self.task is not None:
78+
self.cancel()
79+
self.run_button.setText("Resume")
80+
self.commit()
81+
# Resume task
82+
else:
83+
self._run()
84+
85+
def _run(self):
86+
self.run_button.setText("Stop")
87+
self.start(run, self.data, self.embedding)
88+
89+
# ConcurrentWidgetMixin
90+
def on_partial_result(self, result: Result):
91+
assert isinstance(result.embedding, np.ndarray)
92+
assert len(result.embedding) == len(self.data)
93+
first_result = self.embedding is None
94+
self.embedding = result.embedding
95+
if first_result:
96+
self.setup_plot()
97+
else:
98+
self.graph.update_coordinates()
99+
self.graph.update_density()
100+
101+
def on_done(self, result: Result):
102+
# NOTE: All of these have already been set by on_partial_result,
103+
# we double check that they are aliased
104+
assert isinstance(result.embedding, np.ndarray)
105+
assert len(result.embedding) == len(self.data)
106+
self.embedding = result.embedding
107+
self.run_button.setText("Start")
108+
self.commit()
109+
110+
# OWDataProjectionWidget
111+
def set_data(self, data: Table):
112+
super().set_data(data)
113+
if self._invalidated:
114+
self._run()
115+
116+
def get_embedding(self):
117+
if self.embedding is None:
118+
self.valid_data = None
119+
return None
120+
121+
self.valid_data = np.all(np.isfinite(self.embedding), 1)
122+
return self.embedding
123+
124+
def clear(self):
125+
super().clear()
126+
self.cancel()
127+
self.embedding = None
128+
129+
def onDeleteWidget(self):
130+
self.shutdown()
131+
super().onDeleteWidget()
132+
133+
134+
if __name__ == "__main__":
135+
table = Table("iris")
136+
WidgetPreview(OWConcurrentWidget).run(
137+
set_data=table, set_subset_data=table[::10])
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Test methods with long descriptive names can omit docstrings
2+
# pylint: disable=missing-docstring
3+
import unittest
4+
from unittest.mock import Mock
5+
6+
from Orange.data import Table
7+
from Orange.widgets.tests.base import (
8+
WidgetTest, WidgetOutputsTestMixin, ProjectionWidgetTestMixin
9+
)
10+
from Orange.widgets.utils.tests.concurrent_example import (
11+
OWConcurrentWidget
12+
)
13+
14+
15+
class TestOWConcurrentWidget(WidgetTest, ProjectionWidgetTestMixin,
16+
WidgetOutputsTestMixin):
17+
@classmethod
18+
def setUpClass(cls):
19+
super().setUpClass()
20+
WidgetOutputsTestMixin.init(cls)
21+
22+
cls.signal_name = "Data"
23+
cls.signal_data = cls.data
24+
cls.same_input_output_domain = False
25+
26+
def setUp(self):
27+
self.widget = self.create_widget(OWConcurrentWidget)
28+
29+
def test_button_no_data(self):
30+
self.widget.run_button.click()
31+
self.assertEqual(self.widget.run_button.text(), "Start")
32+
33+
def test_button_with_data(self):
34+
self.send_signal(self.widget.Inputs.data, self.data)
35+
self.assertEqual(self.widget.run_button.text(), "Stop")
36+
self.wait_until_stop_blocking()
37+
self.assertEqual(self.widget.run_button.text(), "Start")
38+
39+
def test_button_toggle(self):
40+
self.send_signal(self.widget.Inputs.data, self.data)
41+
self.widget.run_button.click()
42+
self.assertEqual(self.widget.run_button.text(), "Resume")
43+
44+
def test_plot_once(self):
45+
table = Table("heart_disease")
46+
self.widget.setup_plot = Mock()
47+
self.widget.commit = Mock()
48+
self.send_signal(self.widget.Inputs.data, table)
49+
self.widget.setup_plot.assert_called_once()
50+
self.widget.commit.assert_called_once()
51+
self.wait_until_stop_blocking()
52+
self.widget.setup_plot.reset_mock()
53+
self.widget.commit.reset_mock()
54+
self.send_signal(self.widget.Inputs.data_subset, table[::10])
55+
self.widget.setup_plot.assert_not_called()
56+
self.widget.commit.assert_called_once()
57+
58+
59+
if __name__ == "__main__":
60+
unittest.main()

0 commit comments

Comments
 (0)