Skip to content

Commit 007b6ef

Browse files
authored
Merge pull request #3287 from h-mayorquin/typing_to_set_property_and_annotations
Refactor `set_property` in base
2 parents 7b8f181 + 98f4845 commit 007b6ef

2 files changed

Lines changed: 63 additions & 15 deletions

File tree

src/spikeinterface/core/base.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def id_to_index(self, id) -> int:
164164
def annotate(self, **new_annotations) -> None:
165165
self._annotations.update(new_annotations)
166166

167-
def set_annotation(self, annotation_key, value: Any, overwrite=False) -> None:
167+
def set_annotation(self, annotation_key: str, value: Any, overwrite=False) -> None:
168168
"""This function adds an entry to the annotations dictionary.
169169
170170
Parameters
@@ -192,7 +192,7 @@ def get_preferred_mp_context(self):
192192
"""
193193
return self._preferred_mp_context
194194

195-
def get_annotation(self, key, copy: bool = True) -> Any:
195+
def get_annotation(self, key: str, copy: bool = True) -> Any:
196196
"""
197197
Get a annotation.
198198
Return a copy by default
@@ -205,7 +205,13 @@ def get_annotation(self, key, copy: bool = True) -> Any:
205205
def get_annotation_keys(self) -> List:
206206
return list(self._annotations.keys())
207207

208-
def set_property(self, key, values: Sequence, ids: Optional[Sequence] = None, missing_value: Any = None) -> None:
208+
def set_property(
209+
self,
210+
key,
211+
values: list | np.ndarray | tuple,
212+
ids: list | np.ndarray | tuple | None = None,
213+
missing_value: Any = None,
214+
) -> None:
209215
"""
210216
Set property vector for main ids.
211217
@@ -223,6 +229,7 @@ def set_property(self, key, values: Sequence, ids: Optional[Sequence] = None, mi
223229
Array of values for the property
224230
ids : list/np.array, default: None
225231
List of subset of ids to set the values, default: None
232+
if None which is the default all the ids are set or changed
226233
missing_value : object, default: None
227234
In case the property is set on a subset of values ("ids" not None),
228235
it specifies the how the missing values should be filled.
@@ -240,23 +247,26 @@ def set_property(self, key, values: Sequence, ids: Optional[Sequence] = None, mi
240247
dtype_kind = dtype.kind
241248

242249
if ids is None:
243-
assert values.shape[0] == size
250+
assert (
251+
values.shape[0] == size
252+
), f"Values must have the same size as the main ids: {size} but got array of size {values.shape[0]}"
244253
self._properties[key] = values
245254
else:
246255
ids = np.array(ids)
247-
assert np.unique(ids).size == ids.size, "'ids' are not unique!"
256+
unique_ids = np.unique(ids)
257+
if unique_ids.size != ids.size:
258+
non_unique_ids = [id for id in ids if np.count_nonzero(ids == id) > 1]
259+
raise ValueError(f"IDs are not unique: {non_unique_ids}")
248260

261+
# Not clear where this branch is used, perhaps on aggregation of extractors?
249262
if ids.size < size:
250263
if key not in self._properties:
251-
# create the property with nan or empty
252-
shape = (size,) + values.shape[1:]
253264

254265
if missing_value is None:
255266
if dtype_kind not in self.default_missing_property_values.keys():
256-
raise Exception(
257-
"For values dtypes other than float, string, object or unicode, the missing value "
258-
"cannot be automatically inferred. Please specify it with the 'missing_value' "
259-
"argument."
267+
raise ValueError(
268+
f"Can't infer a natural missing value for dtype {dtype_kind}. "
269+
"Please provide it with the `missing_value` argument"
260270
)
261271
else:
262272
missing_value = self.default_missing_property_values[dtype_kind]
@@ -268,15 +278,18 @@ def set_property(self, key, values: Sequence, ids: Optional[Sequence] = None, mi
268278
"as the values."
269279
)
270280

281+
# create the property with nan or empty
282+
shape = (size,) + values.shape[1:]
271283
empty_values = np.zeros(shape, dtype=dtype)
272284
empty_values[:] = missing_value
273285
self._properties[key] = empty_values
274286
if ids.size == 0:
275287
return
276288
else:
277-
assert dtype_kind == self._properties[key].dtype.kind, (
278-
"Mismatch between existing property dtype " "values dtype."
279-
)
289+
existing_property = self._properties[key].dtype
290+
assert (
291+
dtype_kind == existing_property.kind
292+
), f"Mismatch between existing property dtype {existing_property.kind} and provided values dtype {dtype_kind}."
280293

281294
indices = self.ids_to_indices(ids)
282295
self._properties[key][indices] = values
@@ -285,7 +298,7 @@ def set_property(self, key, values: Sequence, ids: Optional[Sequence] = None, mi
285298
self._properties[key] = np.zeros_like(values, dtype=values.dtype)
286299
self._properties[key][indices] = values
287300

288-
def get_property(self, key, ids: Optional[Iterable] = None) -> np.ndarray:
301+
def get_property(self, key: str, ids: Optional[Iterable] = None) -> np.ndarray:
289302
values = self._properties.get(key, None)
290303
if ids is not None and values is not None:
291304
inds = self.ids_to_indices(ids)

src/spikeinterface/core/tests/test_base.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,41 @@ def test_name_and_repr():
9494
assert "Hz" in rec_str
9595

9696

97+
def test_setting_properties():
98+
99+
num_channels = 5
100+
recording = generate_recording(num_channels=5, durations=[1.0])
101+
channel_ids = ["a", "b", "c", "d", "e"]
102+
recording = recording.rename_channels(new_channel_ids=channel_ids)
103+
104+
complete_values = ["value"] * num_channels
105+
recording.set_property(key="a_property", values=complete_values)
106+
107+
property_in_recording = recording.get_property("a_property")
108+
expected_array = np.array(complete_values)
109+
assert np.array_equal(property_in_recording, expected_array)
110+
111+
# Set property with missing values
112+
incomplete_values = ["value"] * (num_channels - 1)
113+
recording.set_property(key="incomplete_property", ids=channel_ids[:-1], values=incomplete_values)
114+
115+
property_in_recording = recording.get_property("incomplete_property")
116+
expected_array = np.array(incomplete_values + [""]) # Spikeinterface defines missing values as empty strings
117+
assert np.array_equal(property_in_recording, expected_array)
118+
119+
# # Passs a missing value
120+
# recording.set_property(
121+
# key="missing_property",
122+
# ids=channel_ids[:-1],
123+
# values=incomplete_values,
124+
# missing_value="missing",
125+
# )
126+
127+
# property_in_recording = recording.get_property("missing_property")
128+
# expected_array = np.array(incomplete_values + ["missing"])
129+
# assert np.array_equal(property_in_recording, expected_array)
130+
131+
97132
if __name__ == "__main__":
98133
test_check_if_memory_serializable()
99134
test_check_if_serializable()

0 commit comments

Comments
 (0)