Skip to content

Commit 6fa6114

Browse files
committed
OWConcurrentProjectionWidget: Add an example
1 parent ae7cfd1 commit 6fa6114

File tree

3 files changed

+208
-1
lines changed

3 files changed

+208
-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: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# pylint: disable=too-many-ancestors
2+
from typing import Optional
3+
from types import SimpleNamespace as namespace
4+
from functools import partial
5+
6+
import numpy as np
7+
8+
from Orange.data import Table
9+
from Orange.widgets import gui
10+
from Orange.widgets.settings import Setting
11+
from Orange.widgets.utils.concurrent import TaskState, ConcurrentWidgetMixin
12+
from Orange.widgets.utils.widgetpreview import WidgetPreview
13+
from Orange.widgets.visualize.utils.widget import OWDataProjectionWidget
14+
15+
16+
class Results(namespace):
17+
embedding = None # type: Optional[np.ndarray]
18+
19+
20+
class Runner:
21+
@staticmethod
22+
def run(data: Table, embedding: Optional[np.ndarray], state: TaskState):
23+
res = Results(embedding=embedding)
24+
25+
# simulate wasteful calculation (increase 'steps')
26+
step, steps = 0, 10
27+
state.set_status("Calculating...")
28+
while step < steps:
29+
for _ in range(steps):
30+
x_data = np.array(np.mean(data.X, axis=1))
31+
if x_data.ndim == 2:
32+
x_data = x_data.ravel()
33+
y_data = np.ones(len(x_data))
34+
y_data[::2] = step % 2
35+
y_data = np.random.rand(len(x_data))
36+
# needs a copy because embedding can change after it has
37+
# beed send by set_partial_results
38+
embedding = np.vstack((x_data, y_data)).T.copy()
39+
step += 1
40+
if step % (steps / 10) == 0:
41+
state.set_progress_value(100 * step / steps)
42+
43+
if state.is_interruption_requested():
44+
return res
45+
46+
res.embedding = embedding
47+
state.set_partial_results(res)
48+
return res
49+
50+
51+
class OWConcurrentWidget(OWDataProjectionWidget, ConcurrentWidgetMixin):
52+
name = "Projection"
53+
param = Setting(0)
54+
55+
def __init__(self):
56+
OWDataProjectionWidget.__init__(self)
57+
ConcurrentWidgetMixin.__init__(self)
58+
self.embedding = None # type: Optional[np.ndarray]
59+
60+
# GUI
61+
def _add_controls(self):
62+
box = gui.vBox(self.controlArea, True)
63+
gui.comboBox(
64+
box, self, "param", label="Parameter:",
65+
items=["Param A", "Param B"], labelWidth=80,
66+
callback=self.__param_combo_changed
67+
)
68+
self.run_button = gui.button(box, self, "Start", self._toggle_run)
69+
super()._add_controls()
70+
71+
def __param_combo_changed(self):
72+
super().start()
73+
74+
def _toggle_run(self):
75+
if self.data is None:
76+
return
77+
78+
# Pause task
79+
if self.task is not None:
80+
self.cancel()
81+
self.run_button.setText("Resume")
82+
self.commit()
83+
# Resume task
84+
else:
85+
self.start()
86+
87+
# extend ConcurrentWidgetMixin
88+
def _prepare_task(self, state: TaskState):
89+
return partial(Runner.run, self.data,
90+
embedding=self.embedding, state=state)
91+
92+
def _set_partial_results(self, result: Results):
93+
assert isinstance(result.embedding, np.ndarray)
94+
assert len(result.embedding) == len(self.data)
95+
first_result = self.embedding is None
96+
self.embedding = result.embedding
97+
if first_result:
98+
self.setup_plot()
99+
else:
100+
self.graph.update_coordinates()
101+
self.graph.update_density()
102+
103+
def _set_results(self, result: Results):
104+
assert isinstance(result.embedding, np.ndarray)
105+
assert len(result.embedding) == len(self.data)
106+
super()._set_results(result)
107+
108+
def start(self):
109+
self.run_button.setText("Stop")
110+
super().start()
111+
112+
def on_done(self, future):
113+
super().on_done(future)
114+
self.run_button.setText("Start")
115+
self.commit()
116+
117+
# extend OWDataProjectionWidget
118+
def set_data(self, data: Table):
119+
super().set_data(data)
120+
if self._invalidated:
121+
self.start()
122+
123+
def get_embedding(self):
124+
if self.embedding is None:
125+
self.valid_data = None
126+
return None
127+
128+
self.valid_data = np.all(np.isfinite(self.embedding), 1)
129+
return self.embedding
130+
131+
def clear(self):
132+
super().clear()
133+
self.cancel()
134+
self.embedding = None
135+
136+
def onDeleteWidget(self):
137+
self.on_delete_widget()
138+
super().onDeleteWidget()
139+
140+
141+
if __name__ == "__main__":
142+
table = Table("iris")
143+
WidgetPreview(OWConcurrentWidget).run(
144+
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)