Skip to content
27 changes: 25 additions & 2 deletions src/osekit/core/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from matplotlib.transforms import TransformedBbox
from pandas import Timedelta, Timestamp

from osekit.config import global_logging_context as glc
from osekit.core.event import Event
from osekit.utils.core import is_empty_dataclass

Expand All @@ -28,6 +29,7 @@
"min_frequency",
"max_frequency",
"annotation",
"label",
"annotator",
"annotator_expertise",
"start_datetime",
Expand Down Expand Up @@ -617,7 +619,7 @@ def from_dict(cls, row: dict) -> Self:

return cls(
metadata=metadata,
label=row.get("annotation"),
label=row.get("label") or row.get("annotation"),
detector_info=detector_info,
begin=Timestamp(row["start_datetime"]),
end=Timestamp(row["end_datetime"]),
Expand Down Expand Up @@ -764,7 +766,21 @@ def _from_csv(cls, csv: Path, **kwargs: Any) -> list[Self]:
}
for record in records
]
return [cls.from_dict(record) for record in records]
parsed_records = []
failed_records = []
for idx, record in enumerate(records):
try:
parsed_records.append(cls.from_dict(record))
except TypeError: # Most probably a weak comment on the spectrogram
failed_records.append(idx)

if failed_records:
glc.logger.warning(
f"The following line(s) couldn't be parsed as detections: "
f"{failed_records}",
)

return parsed_records

@classmethod
def from_csv(cls, csv: Path | list[Path], **kwargs: Any) -> list[Self]:
Expand Down Expand Up @@ -792,4 +808,11 @@ def from_csv(cls, csv: Path | list[Path], **kwargs: Any) -> list[Self]:
for csv_file in csv:
output += cls._from_csv(csv_file, **kwargs)

if not output:
msg = (
"No valid Detection was found in the given csv file(s).\n"
"Tip: you can specify the used separator with the sep parameter."
)
raise ValueError(msg)

return output
3 changes: 2 additions & 1 deletion tests/_static/aplose_result.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
dataset,filename,annotation_id,is_update_of_id,start_time,end_time,start_frequency,end_frequency,min_frequency,max_frequency,annotation,annotator,annotator_expertise,start_datetime,end_datetime,is_box,type,confidence_indicator_label,confidence_indicator_level,comments,signal_quantity,signal_is_intensity_too_low,signal_does_overlap_other_signals,signal_start_frequency,signal_end_frequency,signal_relative_min_frequency_count,signal_relative_max_frequency_count,signal_steps_count,signal_has_harmonics,signal_trend,signal_sidebands,signal_subharmonics,signal_frequency_jumps,signal_deterministic_chaos,created_at_phase,lookaftering,bunyan
dataset,filename,annotation_id,is_update_of_id,start_time,end_time,start_frequency,end_frequency,min_frequency,max_frequency,label,annotator,annotator_expertise,start_datetime,end_datetime,is_box,type,confidence_indicator_label,confidence_indicator_level,comments,signal_quantity,signal_is_intensity_too_low,signal_does_overlap_other_signals,signal_start_frequency,signal_end_frequency,signal_relative_min_frequency_count,signal_relative_max_frequency_count,signal_steps_count,signal_has_harmonics,signal_trend,signal_sidebands,signal_subharmonics,signal_frequency_jumps,signal_deterministic_chaos,created_at_phase,lookaftering,bunyan
great_tit,990694,586654,,0.0,20.0,0.0,24000.0,0.0,24000.0,bird,vashti,NOVICE,2021-01-01T00:00:00.000+00:00,2021-01-01T00:00:20.000+00:00,0,WEAK,Sure,1/1,great tits |- vashti,MULTIPLE,,,,,,,,,,,,,,ANNOTATION,True,False
great_tit,990694,586655,,1.412,3.651,2512.0,15661.0,2512.0,15661.0,bird,vashti,NOVICE,2021-01-01T00:00:01.412+00:00,2021-01-01T00:00:03.651+00:00,1,BOX,Not sure,0/1,fluffy-backed tit-babbler |- vashti,MULTIPLE,,,,,,,,,,,,,,ANNOTATION,False,False
great_tit,990694,586673,,10.0,12.0,12000.0,13000.0,12000.0,13000.0,rain,heartleap,,2021-01-01T00:00:00.000+00:00,2021-01-01T00:00:20.000+00:00,0,BOX,,,,MULTIPLE,,,,,,,,,,,,,,ANNOTATION,True,True
Expand All @@ -9,3 +9,4 @@ great_tit,990694,586710,586655,1.412,3.651,2512.0,15660.0,2512.0,15660.0,bird,lo
great_tit,994410,586671,,0.0,20.0,0.0,24000.0,0.0,24000.0,car,bunyan,EXPERT,2021-01-01T00:01:18.218+00:00,2021-01-01T00:01:38.218+00:00,0,WEAK,Sure,1/1,,MULTIPLE,,,,,,,,,,,,,,VERIFICATION,,
great_tit,994410,586672,,0.0,20.0,0.0,24000.0,0.0,24000.0,bird,bunyan,EXPERT,2021-01-01T00:01:18.218+00:00,2021-01-01T00:01:38.218+00:00,0,WEAK,Sure,1/1,,MULTIPLE,,,,,,,,,,,,,,VERIFICATION,,
,,,,,,,,0.0,24000.0,,,,2021-01-01T00:01:18.218+00:00,2021-01-01T00:01:38.218+00:00,,,,,,,,,,,,,,,,,,,,,,
great_tit,990694,,,,,,,,,,heartleap,,2021-01-01T00:00:00.000+00:00,2021-01-01T00:00:20.000+00:00,,,,,weak comment |- heartleap,,,,,,,,,,,,,,,,,
2 changes: 2 additions & 0 deletions tests/_static/retro_compatibility_result.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dataset,filename,annotation_id,is_update_of_id,start_time,end_time,start_frequency,end_frequency,min_frequency,max_frequency,annotation,annotator,annotator_expertise,start_datetime,end_datetime,is_box,type,confidence_indicator_label,confidence_indicator_level,comments,signal_quantity,signal_is_intensity_too_low,signal_does_overlap_other_signals,signal_start_frequency,signal_end_frequency,signal_relative_min_frequency_count,signal_relative_max_frequency_count,signal_steps_count,signal_has_harmonics,signal_trend,signal_sidebands,signal_subharmonics,signal_frequency_jumps,signal_deterministic_chaos,created_at_phase,lookaftering,bunyan
great_tit,990694,586654,,0.0,20.0,0.0,24000.0,0.0,24000.0,bird,vashti,NOVICE,2021-01-01T00:00:00.000+00:00,2021-01-01T00:00:20.000+00:00,0,WEAK,Sure,1/1,great tits |- vashti,MULTIPLE,,,,,,,,,,,,,,ANNOTATION,True,False
37 changes: 33 additions & 4 deletions tests/test_detection.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
from contextlib import AbstractContextManager, nullcontext
from pathlib import Path
from typing import Any, Literal

import numpy as np
import pytest
from _pytest.logging import LogCaptureFixture
from matplotlib.axes import Axes
from matplotlib.patches import Rectangle
from pandas import DataFrame, Timedelta, Timestamp
Expand Down Expand Up @@ -235,10 +237,14 @@ def test_confidence_indicator_from_relative_level_string(
assert ci.maximum_level == e.maximum_level


def test_detections_from_csv() -> None:
detections = Detection.from_csv(
csv=Path(__file__).parent / "_static" / "aplose_result.csv",
)
def test_detections_from_csv(caplog: LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING):
detections = Detection.from_csv(
csv=Path(__file__).parent / "_static" / "aplose_result.csv",
)

# Failed parsed line should be logged (weak comment at idx 10)
assert "[10]" in caplog.text

minimal_detection = next(d for d in detections if d.label is None)
detections = [d for d in detections if d is not minimal_detection]
Expand Down Expand Up @@ -336,6 +342,21 @@ def test_detections_from_csv() -> None:
assert len(minimal_detection.verifications) == 0


def test_detections_from_csv_raises_if_no_detection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def mock_from_dict(*args, **kwargs) -> None: # noqa: ANN002, ANN003
msg = r"Mock a TypeError while trying to parse a Detection object."
raise TypeError(msg)

monkeypatch.setattr(Detection, "from_dict", mock_from_dict)

with pytest.raises(ValueError, match="No valid Detection was found"):
Detection.from_csv(
csv=Path(__file__).parent / "_static/minimal_detector_result.csv",
)


def test_detections_to_dict(tmp_path: Path) -> None:
for result_file in ("aplose_result.csv", "minimal_detector_result.csv"):
detections = Detection.from_csv(
Expand Down Expand Up @@ -393,6 +414,14 @@ def test_minimal_detector_result_file() -> None:
assert len(detections) == 2


def test_retro_compatibility() -> None:
detections = Detection.from_csv(
csv=Path(__file__).parent / "_static/retro_compatibility_result.csv",
)

assert detections[0].label == "bird"


def test_detections_from_csv_list() -> None:
detections = Detection.from_csv(
[
Expand Down
Loading