Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion Orange/widgets/data/owcorrelations.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,38 @@ def stopped(self):
header = self.rank_table.horizontalHeader()
header.setSectionResizeMode(1, QHeaderView.Stretch)

def start(self, task, *args, **kwargs):
self.__set_state_ready()
super().start(task, *args, **kwargs)
self.__set_state_busy()

def cancel(self):
super().cancel()
self.__set_state_ready()

def _connect_signals(self, state):
super()._connect_signals(state)
state.progress_changed.connect(self.master.progressBarSet)
state.status_changed.connect(self.master.setStatusMessage)

def _disconnect_signals(self, state):
super()._disconnect_signals(state)
state.progress_changed.disconnect(self.master.progressBarSet)
state.status_changed.disconnect(self.master.setStatusMessage)

def _on_task_done(self, future):
super()._on_task_done(future)
self.__set_state_ready()

def __set_state_ready(self):
self.master.progressBarFinished()
self.master.setBlocking(False)
self.master.setStatusMessage("")

def __set_state_busy(self):
self.master.progressBarInit()
self.master.setBlocking(True)


class OWCorrelations(OWWidget):
name = "Correlations"
Expand Down Expand Up @@ -250,7 +282,6 @@ def __init__(self):

self.vizrank, _ = CorrelationRank.add_vizrank(
None, self, None, self._vizrank_selection_changed)
self.vizrank.progressBar = self.progressBar
self.vizrank.button.setEnabled(False)
self.vizrank.threadStopped.connect(self._vizrank_stopped)

Expand Down
2 changes: 2 additions & 0 deletions Orange/widgets/data/tests/test_owcorrelations.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ def test_feature_combo(self):
if attr.is_continuous]
self.assertEqual(len(feature_combo.model()), len(cont_attributes) + 1)

self.wait_until_stop_blocking()
self.send_signal(self.widget.Inputs.data, Table("housing"))
self.assertEqual(len(feature_combo.model()), 14)

Expand Down Expand Up @@ -281,6 +282,7 @@ def test_send_report(self):
"""Test report """
self.send_signal(self.widget.Inputs.data, self.data_cont)
self.widget.report_button.click()
self.wait_until_stop_blocking()
self.send_signal(self.widget.Inputs.data, None)
self.widget.report_button.click()

Expand Down
3 changes: 3 additions & 0 deletions Orange/widgets/visualize/owlinearprojection.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def __init__(self, master):
gui.rubber(box)
self.last_run_n_attrs = None
self.attr_color = master.attr_color
self.attrs = []

def initialize(self):
super().initialize()
Expand Down Expand Up @@ -95,6 +96,8 @@ def check_preconditions(self):

def state_count(self):
n_all_attrs = len(self.attrs)
if not n_all_attrs:
return 0
n_attrs = self.n_attrs
return factorial(n_all_attrs) // (2 * factorial(n_all_attrs - n_attrs) * n_attrs)

Expand Down
6 changes: 4 additions & 2 deletions Orange/widgets/visualize/tests/test_owlinearprojection.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,15 @@ def setUp(self):
def test_discrete_class(self):
self.send_signal(self.widget.Inputs.data, self.data)
run_vizrank(self.vizrank.compute_score,
self.vizrank.iterate_states(None), [], Mock())
self.vizrank.iterate_states, None,
[], 0, self.vizrank.state_count(), Mock())

def test_continuous_class(self):
data = Table("housing")[::100]
self.send_signal(self.widget.Inputs.data, data)
run_vizrank(self.vizrank.compute_score,
self.vizrank.iterate_states(None), [], Mock())
self.vizrank.iterate_states, None,
[], 0, self.vizrank.state_count(), Mock())

def test_set_attrs(self):
self.send_signal(self.widget.Inputs.data, self.data)
Expand Down
31 changes: 23 additions & 8 deletions Orange/widgets/visualize/tests/test_vizrankdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ def test_run_vizrank(self):
# run through all states
task.is_interruption_requested.return_value = False
states = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
res = run_vizrank(compute_score, chain(states), scores, task)
res = run_vizrank(compute_score, lambda initial: chain(states),
None, scores, 0, 6, task)

next_state = self.assertQueueEqual(
res.queue, [0, 0, 0, 3, 2, 5], compute_score,
Expand All @@ -40,28 +41,33 @@ def test_run_vizrank(self):
res_scores = sorted([compute_score(x) for x in states])
self.assertListEqual(res.scores, res_scores)
self.assertIsNot(scores, res.scores)
self.assertEqual(task.set_partial_result.call_count, 6)
self.assertEqual(task.set_partial_result.call_count, 2)
self.assertEqual(task.set_progress_value.call_count, 7)

def test_run_vizrank_interrupt(self):
scores, task = [], Mock()
# interrupt calculation in third iteration
task.is_interruption_requested.side_effect = lambda: \
True if task.is_interruption_requested.call_count > 2 else False
states = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
res = run_vizrank(compute_score, chain(states), scores, task)
res = run_vizrank(compute_score, lambda initial: chain(states),
None, scores, 0, 6, task)

next_state = self.assertQueueEqual(
res.queue, [0, 0], compute_score, states[:2], states[1:3])
self.assertEqual(next_state, (0, 3))
res_scores = sorted([compute_score(x) for x in states[:2]])
self.assertListEqual(res.scores, res_scores)
self.assertIsNot(scores, res.scores)
self.assertEqual(task.set_partial_result.call_count, 2)
self.assertEqual(task.set_partial_result.call_count, 1)
self.assertEqual(task.set_progress_value.call_count, 3)
task.set_progress_value.assert_called_with(int(1 / 6 * 100))

# continue calculation through all states
task.is_interruption_requested.side_effect = lambda: False
i = states.index(next_state)
res = run_vizrank(compute_score, chain(states[i:]), res_scores, task)
res = run_vizrank(compute_score, lambda initial: chain(states[i:]),
None, res_scores, 2, 6, task)

next_state = self.assertQueueEqual(
res.queue, [0, 3, 2, 5], compute_score, states[2:],
Expand All @@ -70,7 +76,9 @@ def test_run_vizrank_interrupt(self):
res_scores = sorted([compute_score(x) for x in states])
self.assertListEqual(res.scores, res_scores)
self.assertIsNot(scores, res.scores)
self.assertEqual(task.set_partial_result.call_count, 6)
self.assertEqual(task.set_partial_result.call_count, 3)
self.assertEqual(task.set_progress_value.call_count, 8)
task.set_progress_value.assert_called_with(int(5 / 6 * 100))

def assertQueueEqual(self, queue, positions, f, states, next_states):
self.assertIsInstance(queue, Queue)
Expand All @@ -95,8 +103,12 @@ def iterate_states(initial_state):
def invoke_on_partial_result():
widget.on_partial_result(run_vizrank(
widget.compute_score,
widget.iterate_states(widget.saved_state),
widget.scores, task
widget.iterate_states,
widget.saved_state,
widget.scores,
widget.saved_progress,
widget.state_count(),
task
))

task = Mock()
Expand All @@ -107,6 +119,7 @@ def invoke_on_partial_result():
widget.compute_score = compute_score
widget.iterate_states = iterate_states
widget.row_for_state = lambda sc, _: [QStandardItem(str(sc))]
widget.state_count = lambda: len(states)

# interrupt calculation in third iteration
task.is_interruption_requested.side_effect = lambda: \
Expand All @@ -117,6 +130,7 @@ def invoke_on_partial_result():
sorted([compute_score(x) for x in states[:2]])):
self.assertEqual(widget.rank_model.item(row, 0).text(), str(score))
self.assertEqual(widget.saved_progress, 2)
task.set_progress_value.assert_called_with(int(1 / 6 * 100))

# continue calculation through all states
task.is_interruption_requested.side_effect = lambda: False
Expand All @@ -126,6 +140,7 @@ def invoke_on_partial_result():
sorted([compute_score(x) for x in states])):
self.assertEqual(widget.rank_model.item(row, 0).text(), str(score))
self.assertEqual(widget.saved_progress, 6)
task.set_progress_value.assert_called_with(int(5 / 6 * 100))


if __name__ == "__main__":
Expand Down
34 changes: 29 additions & 5 deletions Orange/widgets/visualize/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from operator import attrgetter
from queue import Queue, Empty
from types import SimpleNamespace as namespace
from typing import Optional, Iterable, List, Callable, Iterator
from typing import Optional, Iterable, List, Callable
from threading import Timer

from AnyQt.QtCore import Qt, QSize, pyqtSignal as Signal, QSortFilterProxyModel
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QColor, QBrush, QPen
Expand Down Expand Up @@ -366,7 +367,8 @@ def toggle(self):
self.progressBarInit()
self.before_running()
self.start(run_vizrank, self.compute_score,
self.iterate_states(self.saved_state), self.scores)
self.iterate_states, self.saved_state, self.scores,
self.saved_progress, self.state_count())
else:
self.button.setText("Continue")
self.button.repaint()
Expand All @@ -382,10 +384,17 @@ def stopped(self):
pass


def run_vizrank(compute_score: Callable, states: Iterator,
scores: List, task: TaskState):
def run_vizrank(compute_score: Callable, iterate_states: Callable,
saved_state: Optional[Iterable], scores: List,
progress: int, state_count: int, task: TaskState):
task.set_status("Getting combinations...")
task.set_progress_value(0.1)
states = iterate_states(saved_state)

task.set_status("Getting scores...")
res = Result(queue=Queue(), scores=None)
scores = scores.copy()
can_set_partial_result = True

def do_work(st, next_st):
try:
Expand All @@ -398,19 +407,34 @@ def do_work(st, next_st):
except Exception: # ignore current state in case of any problem
pass
res.scores = scores.copy()
task.set_partial_result(res)

def reset_flag():
nonlocal can_set_partial_result
can_set_partial_result = True

state = None
next_state = next(states)
try:
while True:
if task.is_interruption_requested():
return res
task.set_progress_value(int(progress * 100 / max(1, state_count)))
progress += 1
state = copy.copy(next_state)
next_state = copy.copy(next(states))
do_work(state, next_state)
# for simple scores (e.g. correlations widget) and many feature
# combinations, the 'partial_result_ready' signal (emitted by
# invoking 'task.set_partial_result') was emitted too frequently
# for a longer period of time and therefore causing the widget
# being unresponsive
if can_set_partial_result:
task.set_partial_result(res)
can_set_partial_result = False
Timer(0.01, reset_flag).start()
except StopIteration:
do_work(state, None)
task.set_partial_result(res)
return res


Expand Down