Skip to content

Commit e34d66d

Browse files
committed
Parameter Fitter: Allow dots in sequences
1 parent 272c124 commit e34d66d

File tree

4 files changed

+431
-80
lines changed

4 files changed

+431
-80
lines changed

Orange/widgets/evaluate/owparameterfitter.py

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from Orange.util import dummy_callback
2222
from Orange.widgets import gui
2323
from Orange.widgets.settings import Setting
24+
from Orange.widgets.utils import userinput
2425
from Orange.widgets.utils.concurrent import ConcurrentWidgetMixin, TaskState
2526
from Orange.widgets.utils.multi_target import check_multiple_targets_input
2627
from Orange.widgets.utils.widgetpreview import WidgetPreview
@@ -332,10 +333,7 @@ class Error(OWWidget.Error):
332333
unknown_err = Msg("{}")
333334
not_enough_data = Msg(f"At least {N_FOLD} instances are needed.")
334335
incompatible_learner = Msg("{}")
335-
manual_steps_error = Msg("Invalid list of values for {}:\n{}")
336-
manual_invalid_minmax = Msg("Value for {} must be between {} and {}")
337-
manual_invalid_minimum = Msg("Value for {} must be at least {}")
338-
manual_invalid_maximum = Msg("Value for {} must be at most {}")
336+
manual_steps_error = Msg("Invalid values for '{}': {}")
339337
min_max_error = Msg("Minimum must be less than maximum.")
340338

341339
class Warning(OWWidget.Warning):
@@ -404,7 +402,7 @@ def _add_controls(self):
404402
gui.appendRadioButton(buttons, "Manual:")
405403
layout.addWidget(buttons, 4, 0)
406404
self.edit = gui.lineEdit(None, self, "manual_steps",
407-
placeholderText="10, 20, 30",
405+
placeholderText="e.g. 10, 20, ..., 50",
408406
callback=self.__on_manual_changed)
409407
layout.addWidget(self.edit, 4, 1)
410408

@@ -461,19 +459,16 @@ def initial_parameters(self) -> dict:
461459
return self._learner.params
462460

463461
@property
464-
def steps(self) -> tuple[int]:
462+
def steps(self) -> tuple[int, ...]:
465463
self.Error.min_max_error.clear()
466464
self.Error.manual_steps_error.clear()
467-
self.Error.manual_invalid_minimum.clear()
468-
self.Error.manual_invalid_maximum.clear()
469-
self.Error.manual_invalid_minmax.clear()
470465

471466
if self.type == self.FROM_RANGE:
472467
return self._steps_from_range()
473468
else:
474469
return self._steps_from_manual()
475470

476-
def _steps_from_range(self) -> tuple[int]:
471+
def _steps_from_range(self) -> tuple[int, ...]:
477472
if self.maximum < self.minimum:
478473
self.Error.min_max_error()
479474
return ()
@@ -491,29 +486,14 @@ def _steps_from_range(self) -> tuple[int]:
491486

492487
def _steps_from_manual(self) -> tuple[int, ...]:
493488
param = self.fitted_parameters[self.parameter_index]
494-
steps = self.manual_steps
495-
if not steps:
496-
return ()
497489
try:
498-
steps = tuple(sorted(set(map(int, steps.split(",")))))
499-
except ValueError as exc:
500-
self.Error.manual_steps_error(param.name, exc)
501-
return ()
502-
503-
self.manual_steps = ", ".join(map(str, steps))
504-
505-
under = param.min is not None and steps[0] < param.min
506-
over = param.max is not None and steps[-1] > param.max
507-
if under or over:
508-
if under and over:
509-
self.Error.manual_invalid_minmax(param.name, param.min, param.max)
510-
elif under:
511-
self.Error.manual_invalid_minimum(param.name, param.min)
512-
else:
513-
assert over
514-
self.Error.manual_invalid_maximum(param.name, param.max)
490+
steps = userinput.numbers_from_list(
491+
self.manual_steps, int, param.min, param.max)
492+
except ValueError as ex:
493+
self.Error.manual_steps_error(param.label, ex)
515494
return ()
516-
495+
if steps and "..." not in self.manual_steps:
496+
self.manual_steps = ", ".join(map(str, steps))
517497
return steps
518498

519499
@Inputs.data
@@ -589,7 +569,7 @@ def _set_range_controls(self):
589569
self.__spin_max.setMaximum(MIN_MAX_SPIN)
590570
self.maximum = self.initial_parameters[param.name]
591571

592-
tip = "Enter a list of comma-separated values"
572+
tip = "Enter a list of values"
593573
if param.min is not None:
594574
if param.max is not None:
595575
self.edit.setToolTip(f"{tip} between {param.min} and {param.max}.")

Orange/widgets/evaluate/tests/test_owparameterfitter.py

Lines changed: 121 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,13 @@ def test_manual_steps(self):
137137
def test_manual_steps_limits(self):
138138
w = self.widget
139139

140-
def test_errors(act):
141-
err = w.Error
142-
for error in (err.manual_steps_error, err.manual_invalid_minmax,
143-
err.manual_invalid_minimum, err.manual_invalid_maximum):
144-
self.assertIs(error.is_shown(), error is act)
145-
146-
def enter(text):
147-
w.controls.manual_steps.setText(text)
148-
w.controls.manual_steps.returnPressed.emit()
140+
def check(cases):
141+
for setting, steps in cases:
142+
w.controls.manual_steps.setText(setting)
143+
w.controls.manual_steps.returnPressed.emit()
144+
self.assertEqual(w.steps, steps, f"setting: {setting}")
145+
self.assertIs(w.Error.manual_steps_error.is_shown(), not steps,
146+
f"setting: {setting}")
149147

150148
self.send_signal(w.Inputs.data, self._housing)
151149
self.send_signal(w.Inputs.learner, self._dummy)
@@ -154,61 +152,37 @@ def enter(text):
154152
# 5 to None
155153
simulate.combobox_activate_index(w.controls.parameter_index, 0)
156154
self.wait_until_finished()
157-
158-
enter("6, 9, 7")
159-
self.assertEqual(w.steps, (6, 7, 9))
160-
test_errors(None)
161-
162-
enter("6, 9, 7, 3")
163-
self.assertEqual(w.steps, ())
164-
test_errors(w.Error.manual_invalid_minimum)
165-
166-
enter("6, 9, 7")
167-
self.assertEqual(w.steps, (6, 7, 9))
168-
test_errors(None)
169-
170-
enter("6, 9, 7, 3")
155+
check([("6, 9, 7", (6, 7, 9)),
156+
("6, 9, 7, 3", ()),
157+
("6, 9, 7", (6, 7, 9)),
158+
("6, 9, 7, 3", ())])
171159

172160
# None to 10
173161
simulate.combobox_activate_index(w.controls.parameter_index, 2)
174162
self.wait_until_finished()
163+
self.assertFalse(w.Error.manual_steps_error.is_shown())
175164

176-
test_errors(None)
177-
178-
enter("12, 1, 3, -5")
179-
self.assertEqual(w.steps, ())
180-
test_errors(w.Error.manual_invalid_maximum)
181-
182-
enter("1, 3, -5")
183-
self.assertEqual(w.steps, (-5, 1, 3))
184-
test_errors(None)
185-
186-
enter("12, 1, 3, -5")
165+
check([("12, 1, 3, -5", ()),
166+
("1, 3, -5", (-5, 1, 3)),
167+
("12, 1, 3, -5", ())])
187168

188169
# No limits
189170
simulate.combobox_activate_index(w.controls.parameter_index, 3)
190171
self.wait_until_finished()
172+
191173
self.assertEqual(w.steps, (-5, 1, 3, 12))
192-
test_errors(None)
174+
self.assertFalse(w.Error.manual_steps_error.is_shown())
193175

194176
# 5 to 10
195177
simulate.combobox_activate_index(w.controls.parameter_index, 1)
196178
self.wait_until_finished()
197179

198180
self.assertEqual(w.steps, ())
199-
test_errors(w.Error.manual_invalid_minmax)
200-
201-
enter("12, 8, 7, 5")
202-
self.assertEqual(w.steps, ())
203-
test_errors(w.Error.manual_invalid_maximum)
204-
205-
enter("8, 7, -5")
206-
self.assertEqual(w.steps, ())
207-
test_errors(w.Error.manual_invalid_minimum)
181+
self.assertTrue(w.Error.manual_steps_error.is_shown())
208182

209-
enter("8, 7, 5")
210-
self.assertEqual(w.steps, (5, 7, 8))
211-
test_errors(None)
183+
check([("12, 8, 7, 5", ()),
184+
("8, 7, -5", ()),
185+
("8, 7, 5", (5, 7, 8))])
212186

213187
def test_steps_preview(self):
214188
self.send_signal(self.widget.Inputs.data, self._housing)
@@ -443,7 +417,7 @@ def test_steps_from_manual_error(self):
443417
self.send_signal(w.Inputs.learner, None)
444418
self.assertFalse(w.Error.manual_steps_error.is_shown())
445419

446-
def test_steps_from_manual(self):
420+
def test_steps_from_manual_no_dots(self):
447421
w: OWParameterFitter = self.widget
448422
self.send_signal(w.Inputs.data, self._housing)
449423
self.send_signal(w.Inputs.learner, self._dummy)
@@ -460,6 +434,104 @@ def test_steps_from_manual(self):
460434
w.manual_steps = "1, 2, 10, 3, 4, 123, 5, 6"
461435
self.assertEqual(w.steps, (1, 2, 3, 4, 5, 6, 10, 123))
462436

437+
def test_steps_from_manual_dots(self):
438+
def check(cases):
439+
for settings, steps in cases:
440+
w.manual_steps = settings
441+
self.assertEqual(w.steps, steps, f"setting: {settings}")
442+
self.assertIs(w.Error.manual_steps_error.is_shown(), not steps,
443+
f"setting: {settings}")
444+
445+
w: OWParameterFitter = self.widget
446+
self.send_signal(w.Inputs.data, self._housing)
447+
self.send_signal(w.Inputs.learner, self._dummy)
448+
self.wait_until_finished()
449+
w.type = w.MANUAL
450+
451+
# No limits
452+
simulate.combobox_activate_index(w.controls.parameter_index, 3)
453+
self.wait_until_finished()
454+
check([("1, 2, ..., 5", (1, 2, 3, 4, 5)),
455+
("1, 2, 3, ..., 5, 6, 7", (1, 2, 3, 4, 5, 6, 7)),
456+
("3, ..., 5, 6", (3, 4, 5, 6)),
457+
("..., 5, 6", ()),
458+
("5, 6, ...", ()),
459+
("1, 2, 3, 4, 5, ...", ()),
460+
("1, ..., 5", ()),
461+
("1, 2, ..., 5, 6, ..., 8", ())])
462+
463+
# 5 to 10
464+
simulate.combobox_activate_index(w.controls.parameter_index, 1)
465+
self.wait_until_finished()
466+
check([("4, 5, ..., 8", ()),
467+
("5, 6, ..., 12", ()),
468+
("5, 6, ..., 9", (5, 6, 7, 8, 9)),
469+
("6, 7, ..., 9", (6, 7, 8, 9)),
470+
("6, 7, ..., 8, 9", (6, 7, 8, 9)),
471+
("..., 8, 9", (5, 6, 7, 8, 9)),
472+
("6, 7, ...", (6, 7, 8, 9, 10)),
473+
("6, 7, 8, 9, ...", (6, 7, 8, 9, 10)),
474+
("..., 8, 9", (5, 6, 7, 8, 9)),
475+
])
476+
477+
# 5 to None
478+
simulate.combobox_activate_index(w.controls.parameter_index, 0)
479+
self.wait_until_finished()
480+
check([("4, 5, ..., 8", ()),
481+
("5, 6, ..., 12", (5, 6, 7, 8, 9, 10, 11, 12)),
482+
("6, 7, ..., 9", (6, 7, 8, 9)),
483+
("6, 7, ..., 8, 9", (6, 7, 8, 9)),
484+
("..., 8, 9", (5, 6, 7, 8, 9)),
485+
("6, 7, ...", ())
486+
])
487+
488+
# None to 10
489+
simulate.combobox_activate_index(w.controls.parameter_index, 2)
490+
self.wait_until_finished()
491+
check([("4, 5, ..., 8", (4, 5, 6, 7, 8)),
492+
("5, 6, ..., 12", ()),
493+
("5, 6, ..., 9", (5, 6, 7, 8, 9)),
494+
("6, 7, ..., 9", (6, 7, 8, 9)),
495+
("..., 8, 9", ()),
496+
("6, 7, ...", (6, 7, 8, 9, 10)),
497+
("6, 7, 8, 9, ...", (6, 7, 8, 9, 10)),
498+
("..., 8, 9", ()),
499+
])
500+
501+
def test_steps_from_manual_dots_corrections(self):
502+
w: OWParameterFitter = self.widget
503+
self.send_signal(w.Inputs.data, self._housing)
504+
self.send_signal(w.Inputs.learner, self._dummy)
505+
self.wait_until_finished()
506+
w.type = w.MANUAL
507+
508+
# 5 to 10
509+
simulate.combobox_activate_index(w.controls.parameter_index, 1)
510+
self.wait_until_finished()
511+
512+
for settings, steps in [
513+
("5, 6..., 8", (5,6,7,8)),
514+
("5,6...,8", (5,6,7,8)),
515+
("5,6...8", (5,6,7,8)),
516+
("5, 6 ... 8", (5,6,7,8)),
517+
("5, 6 ... 8", (5,6,7,8)),
518+
("5, 6 ... ", (5,6,7,8,9,10)),
519+
("..., 7, 8", (5,6,7,8)),
520+
("..., 7, 8, ...", ()),
521+
("5, 6, ..., 7, 8, ...", ()),
522+
("5, 6, 8, ...", ()),
523+
("5, 6, 8, ...", ()),
524+
("5, 6, ..., 8, 10", ()),
525+
("5, 7, ..., 8, 10", ()),
526+
("8, 7, 6, ...", ()),
527+
("5, 6, 7, ..., 7, 8", ()),
528+
]:
529+
w.manual_steps = settings
530+
self.assertEqual(w.steps, steps, f"setting: {settings}")
531+
self.assertIs(w.Error.manual_steps_error.is_shown(), not steps,
532+
f"setting: {settings}")
533+
534+
463535
def test_manual_tooltip(self):
464536
w: OWParameterFitter = self.widget
465537
self.send_signal(w.Inputs.data, self._housing)
@@ -478,5 +550,6 @@ def test_manual_tooltip(self):
478550
simulate.combobox_activate_index(w.controls.parameter_index, 3)
479551
self.assertEqual("", w.edit.toolTip())
480552

553+
481554
if __name__ == "__main__":
482555
unittest.main()

0 commit comments

Comments
 (0)