Skip to content

Commit 3af1010

Browse files
authored
Merge pull request NeuralEnsemble#1068 from INM-6/feature/newFilter
Feature/new filter
2 parents a0a3bef + dedfc1b commit 3af1010

File tree

5 files changed

+353
-21
lines changed

5 files changed

+353
-21
lines changed

doc/source/authors.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ and may not be the current affiliation of a contributor.
6060
* Etienne Combrisson [6]
6161
* Ben Dichter [24]
6262
* Elodie Legouée [21]
63+
* Oliver Kloss [13]
6364
* Heberto Mayorquin [24]
6465
* Thomas Perret [25]
6566
* Kyle Johnsen [26, 27]

neo/core/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
Classes:
1111
1212
.. autoclass:: Block
13+
.. automethod:: Block.filter
1314
.. autoclass:: Segment
15+
.. automethod:: Segment.filter
1416
.. autoclass:: Group
1517
1618
.. autoclass:: AnalogSignal
@@ -35,6 +37,9 @@
3537
from neo.core.analogsignal import AnalogSignal
3638
from neo.core.irregularlysampledsignal import IrregularlySampledSignal
3739

40+
# Import FilterClasses
41+
from neo.core import filters
42+
3843
from neo.core.event import Event
3944
from neo.core.epoch import Epoch
4045

neo/core/container.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"""
77

88
from copy import deepcopy
9+
10+
from neo.core import filters
911
from neo.core.baseneo import BaseNeo, _reference_name, _container_name
1012
from neo.core.objectlist import ObjectList
1113
from neo.core.spiketrain import SpikeTrain
@@ -21,24 +23,25 @@ def unique_objs(objs):
2123
return [obj for obj in objs
2224
if id(obj) not in seen and not seen.add(id(obj))]
2325

24-
2526
def filterdata(data, targdict=None, objects=None, **kwargs):
2627
"""
2728
Return a list of the objects in data matching *any* of the search terms
2829
in either their attributes or annotations. Search terms can be
2930
provided as keyword arguments or a dictionary, either as a positional
30-
argument after data or to the argument targdict. targdict can also
31-
be a list of dictionaries, in which case the filters are applied
32-
sequentially. If targdict and kwargs are both supplied, the
33-
targdict filters are applied first, followed by the kwarg filters.
34-
A targdict of None or {} and objects = None corresponds to no filters
35-
applied, therefore returning all child objects.
36-
Default targdict and objects is None.
31+
argument after data or to the argument targdict.
32+
A key of a provided dictionary is the name of the requested annotation
33+
and the value is a FilterCondition object.
34+
E.g.: Equal(x), LessThan(x), InRange(x, y).
3735
36+
targdict can also
37+
be a list of dictionaries, in which case the filters are applied
38+
sequentially.
3839
39-
objects (optional) should be the name of a Neo object type,
40-
a neo object class, or a list of one or both of these. If specified,
41-
only these objects will be returned.
40+
A list of dictionaries is handled as follows: [ { or } and { or } ]
41+
If targdict and kwargs are both supplied, the
42+
targdict filters are applied first, followed by the kwarg filters.
43+
A targdict of None or {} corresponds to no filters applied, therefore
44+
returning all child objects. Default targdict is None.
4245
"""
4346

4447
# if objects are specified, get the classes
@@ -72,20 +75,26 @@ def filterdata(data, targdict=None, objects=None, **kwargs):
7275
else:
7376
# do the actual filtering
7477
results = []
75-
for key, value in sorted(targdict.items()):
76-
for obj in data:
77-
if (hasattr(obj, key) and getattr(obj, key) == value and
78-
all([obj is not res for res in results])):
78+
for obj in data:
79+
for key, value in sorted(targdict.items()):
80+
if hasattr(obj, key) and getattr(obj, key) == value:
7981
results.append(obj)
80-
elif (key in obj.annotations and obj.annotations[key] == value and
81-
all([obj is not res for res in results])):
82+
break
83+
if isinstance(value, filters.FilterCondition) and key in obj.annotations:
84+
if value.evaluate(obj.annotations[key]):
85+
results.append(obj)
86+
break
87+
if key in obj.annotations and obj.annotations[key] == value:
8288
results.append(obj)
89+
break
90+
91+
# remove duplicates from results
92+
results = list({ id(res): res for res in results }.values())
8393

8494
# keep only objects of the correct classes
8595
if objects:
8696
results = [result for result in results if
87-
result.__class__ in objects or
88-
result.__class__.__name__ in objects]
97+
result.__class__ in objects or result.__class__.__name__ in objects]
8998

9099
if results and all(isinstance(obj, SpikeTrain) for obj in results):
91100
return SpikeTrainList(results)
@@ -366,9 +375,17 @@ def filter(self, targdict=None, data=True, container=False, recursive=True,
366375
Return a list of child objects matching *any* of the search terms
367376
in either their attributes or annotations. Search terms can be
368377
provided as keyword arguments or a dictionary, either as a positional
369-
argument after data or to the argument targdict. targdict can also
378+
argument after data or to the argument targdict.
379+
A key of a provided dictionary is the name of the requested annotation
380+
and the value is a FilterCondition object.
381+
E.g.: equal(x), less_than(x), InRange(x, y).
382+
383+
targdict can also
370384
be a list of dictionaries, in which case the filters are applied
371-
sequentially. If targdict and kwargs are both supplied, the
385+
sequentially.
386+
387+
A list of dictionaries is handled as follows: [ { or } and { or } ]
388+
If targdict and kwargs are both supplied, the
372389
targdict filters are applied first, followed by the kwarg filters.
373390
A targdict of None or {} corresponds to no filters applied, therefore
374391
returning all child objects. Default targdict is None.
@@ -391,6 +408,8 @@ def filter(self, targdict=None, data=True, container=False, recursive=True,
391408
>>> obj.filter(name="Vm")
392409
>>> obj.filter(objects=neo.SpikeTrain)
393410
>>> obj.filter(targdict={'myannotation':3})
411+
>>> obj.filter(name=neo.core.filters.Equal(5))
412+
>>> obj.filter({'name': neo.core.filters.LessThan(5)})
394413
"""
395414

396415
if isinstance(targdict, str):

neo/core/filters.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""
2+
This module implements :class:`FilterCondition`, which enables use of different filter conditions
3+
for neo.core.container.filter.
4+
5+
Classes:
6+
- :class:`FilterCondition`: Abstract base class for defining filter conditions.
7+
- :class:`Equals`: Filter condition to check if a value is equal to the control value.
8+
- :class:`IsNot`: Filter condition to check if a value is not equal to the control value.
9+
- :class:`LessThanOrEquals`: Filter condition to check if a value is less than or equal to the
10+
control value.
11+
- :class:`GreaterThanOrEquals`: Filter condition to check if a value is greater than or equal to
12+
the control value.
13+
- :class:`LessThan`: Filter condition to check if a value is less than the control value.
14+
- :class:`GreaterThan`: Filter condition to check if a value is greater than the control value.
15+
- :class:`IsIn`: Filter condition to check if a value is in a list or equal to the control
16+
value.
17+
- :class:`InRange`: Filter condition to check if a value is in a specified range.
18+
19+
The provided classes allow users to select filter conditions and use them with
20+
:func:`neo.core.container.filter()` to perform specific filtering operations on data.
21+
"""
22+
from abc import ABC, abstractmethod
23+
from numbers import Number
24+
from typing import Union, Any
25+
26+
27+
class FilterCondition(ABC):
28+
"""
29+
FilterCondition object is given as parameter to container.filter():
30+
31+
Usage:
32+
segment.filter(my_annotation=<FilterCondition>) or
33+
segment=filter({'my_annotation': <FilterCondition>})
34+
"""
35+
@abstractmethod
36+
def __init__(self, control: Any) -> None:
37+
"""
38+
Initialize new FilterCondition object.
39+
40+
Parameters:
41+
control: Any - The control value to be used for filtering.
42+
43+
This is an abstract base class and should not be instantiated directly.
44+
"""
45+
46+
@abstractmethod
47+
def evaluate(self, compare: Any) -> bool:
48+
"""
49+
Evaluate the filter condition for given value.
50+
51+
Parameters:
52+
compare: Any - The value to be compared with the control value.
53+
54+
Returns:
55+
bool: True if the condition is satisfied, False otherwise.
56+
57+
This method should be implemented in subclasses.
58+
"""
59+
60+
61+
class Equals(FilterCondition):
62+
"""
63+
Filter condition to check if target value is equal to the control value.
64+
"""
65+
def __init__(self, control: Any) -> None:
66+
self.control = control
67+
68+
def evaluate(self, compare: Any) -> bool:
69+
return compare == self.control
70+
71+
72+
class IsNot(FilterCondition):
73+
"""
74+
Filter condition to check if target value is not equal to the control value.
75+
"""
76+
def __init__(self, control: Any) -> None:
77+
self.control = control
78+
79+
def evaluate(self, compare: Any) -> bool:
80+
return compare != self.control
81+
82+
83+
class LessThanOrEquals(FilterCondition):
84+
"""
85+
Filter condition to check if target value is less than or equal to the control value.
86+
"""
87+
def __init__(self, control: Number) -> None:
88+
self.control = control
89+
90+
def evaluate(self, compare: Number) -> bool:
91+
return compare <= self.control
92+
93+
94+
class GreaterThanOrEquals(FilterCondition):
95+
"""
96+
Filter condition to check if target value is greater than or equal to the control value.
97+
"""
98+
def __init__(self, control: Number) -> None:
99+
self.control = control
100+
101+
def evaluate(self, compare: Number) -> bool:
102+
return compare >= self.control
103+
104+
105+
class LessThan(FilterCondition):
106+
"""
107+
Filter condition to check if target value is less than the control value.
108+
"""
109+
def __init__(self, control: Number) -> None:
110+
self.control = control
111+
112+
def evaluate(self, compare: Number) -> bool:
113+
return compare < self.control
114+
115+
116+
class GreaterThan(FilterCondition):
117+
"""
118+
Filter condition to check if target value is greater than the control value.
119+
"""
120+
def __init__(self, control: Number) -> None:
121+
self.control = control
122+
123+
def evaluate(self, compare: Number) -> bool:
124+
return compare > self.control
125+
126+
127+
class IsIn(FilterCondition):
128+
"""
129+
Filter condition to check if target is in control.
130+
"""
131+
def __init__(self, control: Union[list, tuple, set, int]) -> None:
132+
self.control = control
133+
134+
def evaluate(self, compare: Any) -> bool:
135+
if isinstance(self.control, (list, tuple, set)):
136+
return compare in self.control
137+
if isinstance(self.control, int):
138+
return compare == self.control
139+
140+
raise SyntaxError('parameter not of type list, tuple, set or int')
141+
142+
143+
class InRange(FilterCondition):
144+
"""
145+
Filter condition to check if a value is in a specified range.
146+
147+
Usage:
148+
InRange(upper_bound, upper_bound, left_closed=False, right_closed=False)
149+
150+
Parameters:
151+
lower_bound: int - The lower bound of the range.
152+
upper_bound: int - The upper bound of the range.
153+
left_closed: bool - If True, the range includes the lower bound (lower_bound <= compare).
154+
right_closed: bool - If True, the range includes the upper bound (compare <= upper_bound).
155+
"""
156+
def __init__(self, lower_bound: Number, upper_bound: Number,
157+
left_closed: bool=False, right_closed: bool=False) -> None:
158+
if not isinstance(lower_bound, Number) or not isinstance(upper_bound, Number):
159+
raise ValueError("parameter is not a number")
160+
161+
self.lower_bound = lower_bound
162+
self.upper_bound = upper_bound
163+
self.left_closed = left_closed
164+
self.right_closed = right_closed
165+
166+
def evaluate(self, compare: Number) -> bool:
167+
if not self.left_closed and not self.right_closed:
168+
return self.lower_bound <= compare <= self.upper_bound
169+
if not self.left_closed and self.right_closed:
170+
return self.lower_bound <= compare < self.upper_bound
171+
if self.left_closed and not self.right_closed:
172+
return self.lower_bound < compare <= self.upper_bound
173+
return self.lower_bound < compare < self.upper_bound

0 commit comments

Comments
 (0)