Skip to content

Commit 0f5b35f

Browse files
committed
fix: removing a box filter did not clear the filter in some cases
1 parent d56e9d0 commit 0f5b35f

File tree

6 files changed

+224
-88
lines changed

6 files changed

+224
-88
lines changed

CHANGELOG

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
2.25.1
2+
- fix: removing a box filter did not clear the filter in some cases
3+
- ref: do not allow specifying filters in `FilterRay.get_dataset()`
4+
- ref: unify hashing caching of datasets in `FilterRay.segments`
5+
- ref: remove ambiguous `FilterRay.root_child` workaround
6+
- ref: simplify `FilterRay.get_final_child`
7+
- tests: improve `FilterRay` test coverage
8+
- setup: bump dclab to 0.67.4 (correct `reset_filter` method)
19
2.25.0
210
- feat: increase width of dataset tile in block matrix via settings (#225)
311
- fix: IndexError when trying to view the logs of a log-less dataset

dcscope/pipeline/core.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,9 @@ def get_dataset(self, slot_index, filt_index=-1, apply_filter=True):
464464
filters = self.get_filters_for_slot(slot_id=slot_id,
465465
max_filter_index=filt_index)
466466
# filter ray magic
467-
ray = self.get_ray(slot.identifier)
468-
ds = ray.get_dataset(filters, apply_filter=apply_filter)
467+
ray = self.get_ray(slot_id)
468+
ray.set_filters(filters)
469+
ds = ray.get_dataset(apply_filter=apply_filter)
469470
return ds
470471

471472
def get_datasets(self, filt_index=-1, apply_filter=True):

dcscope/pipeline/filter_ray.py

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,28 @@ def __init__(self, slot):
1212
self.identifier = slot.identifier
1313
#: slot defining the ray
1414
self.slot = slot
15-
#: list of RTDCBase (hierarchy children)
16-
self.steps = []
17-
#: corresponds to hashes of the applied filters
18-
self.step_hashes = []
15+
#: segments of the filter ray, consisting of hash, previous, and
16+
#: next dataset
17+
self.segments = []
1918
# holds the filters (protected so that users use set_filters)
2019
self._filters = []
2120
# used for testing (incremented when the ray is cut)
2221
self._generation = 0
2322
# used for checking validity of the ray
2423
self._slot_hash = "unset"
25-
self._root_child = None
2624

2725
def __repr__(self):
2826
repre = "<Pipeline Filter Ray '{}' at {}>".format(self.identifier,
2927
hex(id(self)))
3028
return repre
3129

32-
def _add_step(self, ds, filt):
33-
"""Add a filter step"""
34-
self.step_hashes.append(filt.hash)
30+
def _add_segment(self, ds, filt):
31+
"""Add a filter segment"""
32+
ds.reset_filter()
3533
filt.update_dataset(ds)
36-
self.steps.append(ds)
37-
return self._new_child(ds, filt)
34+
child = self._new_child(ds, filt)
35+
self.segments.append([filt.hash, ds, child])
36+
return child
3837

3938
def _new_child(self, ds, filt=None, apply_filter=False):
4039
identifier = self.slot.identifier
@@ -58,21 +57,7 @@ def filters(self):
5857
"""
5958
return self._filters
6059

61-
@property
62-
def root_child(self):
63-
"""This is the first element in self.steps
64-
(Will return a dataset even if self.steps is empty)
65-
"""
66-
if self._slot_hash != self.slot.hash:
67-
# reset everything (e.g. emodulus recipe might have changed)
68-
self.steps = []
69-
self.step_hashes = []
70-
self._root_child = self._new_child(self.slot.get_dataset(),
71-
apply_filter=True)
72-
self._slot_hash = self.slot.hash
73-
return self._root_child
74-
75-
def get_final_child(self, rtdc_ds=None, apply_filter=True):
60+
def get_final_child(self, rtdc_ds=None, filters=None, apply_filter=True):
7661
"""Return the final ray child of `rtdc_ds`
7762
7863
If `rtdc_ds` is None, then the dataset of the current
@@ -86,19 +71,27 @@ def get_final_child(self, rtdc_ds=None, apply_filter=True):
8671
is applied to other data on disk e.g. when computing
8772
statistics. For regular use of the filter ray in a
8873
pipeline, use :func:`get_dataset`.
74+
75+
.. versionchanged:: 2.25.1
76+
The dataset returned is a clean child dataset without any
77+
filters defined.
78+
8979
"""
90-
filters = self.filters
80+
if filters is None:
81+
filters = self.filters
82+
external_filt = False
83+
else:
84+
external_filt = True
9185

9286
if rtdc_ds is None:
9387
# normal case
94-
external = False
95-
rtdc_ds = self.slot.get_dataset()
96-
ds = self.root_child
88+
external_ds = False
89+
ds = self.slot.get_dataset()
9790
else:
9891
# ray is applied to other data
99-
external = True
100-
# do not modify rtdc_ds (create a child to work with)
101-
ds = self._new_child(rtdc_ds, apply_filter=True)
92+
external_ds = True
93+
# do not modify the original dataset (create a child to work with)
94+
ds = self._new_child(rtdc_ds)
10295

10396
# Dear future self,
10497
#
@@ -107,48 +100,46 @@ def get_final_child(self, rtdc_ds=None, apply_filter=True):
107100
# Sincerely,
108101
# past self
109102

103+
filters = [f for f in filters if f.filter_used]
104+
110105
if filters:
111106
# apply all filters
112107
for ii, filt in enumerate(filters):
113108
# remember the previous hierarchy parent
114109
# (ds is always used for the next iteration)
115-
prev_ds = ds
116-
if external:
117-
# do not touch self.steps or self.step_hashes
110+
if external_ds or external_filt:
111+
# do not touch self.segments
118112
filt.update_dataset(ds)
119113
ds = self._new_child(ds, filt)
120-
elif len(self.steps) < ii+1:
121-
# just create a new step
122-
ds = self._add_step(ds, filt)
123-
elif filt.hash != self.step_hashes[ii]:
114+
elif len(self.segments) < ii + 1:
115+
# just create a new segment
116+
ds = self._add_segment(ds, filt)
117+
elif filt.hash != self.segments[ii][0]:
124118
# the filter ray is changing here;
125-
# cut it and add a new step
126-
self.steps = self.steps[:ii]
127-
self.step_hashes = self.step_hashes[:ii]
128-
ds = self._add_step(ds, filt)
119+
# trim it and add a new segment
120+
self.segments = self.segments[:ii]
121+
ds = self._add_segment(ds, filt)
129122
self._generation += 1 # for testing
130123
else:
131-
# the filters match so far
132-
if len(self.steps) > ii + 1: # next child exists
133-
ds = self.steps[ii + 1]
134-
else: # next child does not exist
135-
ds = self._new_child(ds, filt)
136-
# we now have the entire filter pipeline in self.steps
137-
final_ds = prev_ds
124+
# reuse previous segment
125+
ds = self.segments[ii][2]
126+
final_ds = ds
138127
else:
139-
final_ds = rtdc_ds
128+
final_ds = ds
129+
130+
if not external_ds:
131+
ds.reset_filter()
132+
140133
if apply_filter:
141134
final_ds.apply_filter()
135+
142136
return final_ds
143137

144-
def get_dataset(self, filters=None, apply_filter=True):
138+
def get_dataset(self, apply_filter=True):
145139
"""Return the dataset that corresponds to applying these filters
146140
147141
Parameters
148142
----------
149-
filters: list of Filter or None
150-
Filters used for computing the dataset hierarchy. If set
151-
to None, the current filters in `self.filters` are used.
152143
apply_filter: bool
153144
Whether to apply all filters and update the metadata of
154145
the requested dataset. This should be True if you are
@@ -157,14 +148,11 @@ def get_dataset(self, filters=None, apply_filter=True):
157148
apply some more filters and then call `rejuvenate`
158149
yourself.
159150
"""
160-
if filters is not None:
161-
# put the filters in place
162-
self.set_filters(filters)
163151
# compute the final hierarchy child
164152
ds = self.get_final_child(apply_filter=apply_filter)
165153
return ds
166154

167155
def set_filters(self, filters):
168156
"""Set the filters of the current ray"""
169157
# only take into account active filters
170-
self._filters = [f for f in filters if f.filter_used]
158+
self._filters = filters

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ classifiers = [
3030
]
3131
license = "GPL-3.0-or-later"
3232
dependencies = [
33-
"dclab[dcor,export,http,s3]>=0.67.3",
33+
"dclab[dcor,export,http,s3]>=0.67.4",
3434
"h5py>=2.8.0",
3535
"numpy>=1.21", # CVE-2021-33430
3636
"pygments",

tests/test_gui_filter.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,104 @@ def test_box_filter_selection_no_preselection_issue_67(qtbot):
101101
qtbot.mouseClick(wf.toolButton_moreless, QtCore.Qt.MouseButton.LeftButton)
102102

103103

104+
def test_filter_min_max_delete(qtbot):
105+
"""Create a box filter, apply it, and delete it again"""
106+
path = make_fake_dataset()
107+
108+
mw = DCscope()
109+
qtbot.addWidget(mw)
110+
111+
# add the file
112+
mw.add_dataslot(paths=[path, path])
113+
114+
assert len(mw.pipeline.slot_ids) == 2, "we added that"
115+
assert len(mw.pipeline.filter_ids) == 1, "automatically added"
116+
# sanity check
117+
ds10 = dclab.new_dataset(mw.pipeline.get_dataset(0))
118+
ds20 = dclab.new_dataset(mw.pipeline.get_dataset(0))
119+
assert np.max(ds10["area_um"]) > 20
120+
assert np.max(ds20["area_um"]) > 20
121+
122+
# open the filter edit in the Analysis View
123+
fe = mw.block_matrix.get_widget(filt_plot_id=mw.pipeline.filter_ids[0])
124+
qtbot.mouseClick(fe.toolButton_modify, QtCore.Qt.MouseButton.LeftButton)
125+
126+
# box filtering
127+
wf = mw.widget_ana_view.widget_filter
128+
mw.widget_ana_view.tabWidget.setCurrentWidget(
129+
mw.widget_ana_view.tab_filter)
130+
131+
# enable selection
132+
qtbot.mouseClick(wf.toolButton_moreless, QtCore.Qt.MouseButton.LeftButton)
133+
# find the porosity item and click the checkbox
134+
rc = wf._box_range_controls["area_um"]
135+
qtbot.mouseClick(rc.checkBox, QtCore.Qt.MouseButton.LeftButton)
136+
# disable selection
137+
qtbot.mouseClick(wf.toolButton_moreless, QtCore.Qt.MouseButton.LeftButton)
138+
139+
# set the range control for area from 20 to 30 µm
140+
rc.doubleSpinBox_min.setValue(20)
141+
rc.doubleSpinBox_max.setValue(30)
142+
143+
# click apply
144+
qtbot.mouseClick(wf.pushButton_apply, QtCore.Qt.MouseButton.LeftButton)
145+
146+
# make sure this worked
147+
assert rc.read_pipeline_state()["end"] == 30
148+
box_filters = mw.pipeline.filters[0].__getstate__()["box filters"]
149+
assert box_filters["area_um"]["end"] == 30
150+
assert box_filters["area_um"]["active"] is True
151+
152+
# activate the filter for both datasets
153+
me1 = mw.block_matrix.get_widget(filt_plot_id=mw.pipeline.filter_ids[0],
154+
slot_id=mw.pipeline.slot_ids[0]
155+
)
156+
qtbot.mouseClick(me1, QtCore.Qt.MouseButton.LeftButton)
157+
me2 = mw.block_matrix.get_widget(filt_plot_id=mw.pipeline.filter_ids[0],
158+
slot_id=mw.pipeline.slot_ids[1]
159+
)
160+
qtbot.mouseClick(me2, QtCore.Qt.MouseButton.LeftButton)
161+
162+
QtWidgets.QApplication.processEvents(
163+
QEventLoop.ProcessEventsFlag.AllEvents, 500)
164+
165+
assert len(mw.pipeline.get_filters_for_slot(mw.pipeline.slot_ids[0])) == 1
166+
assert len(mw.pipeline.get_filters_for_slot(mw.pipeline.slot_ids[1])) == 1
167+
168+
# get the datasets from the pipeline and check that they are filtered
169+
ds1a = dclab.new_dataset(mw.pipeline.get_dataset(0))
170+
ds2a = dclab.new_dataset(mw.pipeline.get_dataset(1))
171+
172+
# This is expected
173+
assert np.max(ds1a["area_um"]) < 30
174+
assert np.max(ds2a["area_um"]) < 30
175+
176+
# Now, remove the box filter!
177+
qtbot.mouseClick(wf.toolButton_moreless, QtCore.Qt.MouseButton.LeftButton)
178+
# find the porosity item and click the checkbox
179+
rc = wf._box_range_controls["area_um"]
180+
qtbot.mouseClick(rc.checkBox, QtCore.Qt.MouseButton.LeftButton)
181+
# disable selection
182+
qtbot.mouseClick(wf.toolButton_moreless, QtCore.Qt.MouseButton.LeftButton)
183+
184+
# click apply
185+
qtbot.mouseClick(wf.pushButton_apply, QtCore.Qt.MouseButton.LeftButton)
186+
QtWidgets.QApplication.processEvents(
187+
QEventLoop.ProcessEventsFlag.AllEvents, 500)
188+
189+
# make sure this worked
190+
box_filters = mw.pipeline.filters[0].__getstate__()["box filters"]
191+
assert "area_um" not in box_filters
192+
193+
# get the datasets from the pipeline and check that they are filtered
194+
ds1b = dclab.new_dataset(mw.pipeline.get_dataset(0))
195+
ds2b = dclab.new_dataset(mw.pipeline.get_dataset(1))
196+
197+
# This was the bug. The filters were not removed correctly.
198+
assert np.max(ds1b["area_um"]) > 30
199+
assert np.max(ds2b["area_um"]) > 30
200+
201+
104202
def test_filter_min_max_inf(qtbot):
105203
path = make_fake_dataset()
106204

@@ -199,7 +297,7 @@ def test_polygon_filter_basic(qtbot):
199297
ds = mw.pipeline.get_dataset(0)
200298
assert ds_slot is not ds
201299
assert np.sum(ds.filter.all) == 5
202-
assert len(ds) == 47
300+
assert len(ds) == 5
203301

204302

205303
def test_polygon_filter_delete(qtbot):

0 commit comments

Comments
 (0)