From 6a351edb60c3356348e65cb6bceed21dcbebf5f2 Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 09:04:10 +0100 Subject: [PATCH 01/16] Define properties first and last track occurrence in TrackDataset --- OTAnalytics/domain/track_dataset.py | 11 +++++++++++ OTAnalytics/plugin_datastore/python_track_store.py | 8 ++++++++ OTAnalytics/plugin_datastore/track_store.py | 8 ++++++++ 3 files changed, 27 insertions(+) diff --git a/OTAnalytics/domain/track_dataset.py b/OTAnalytics/domain/track_dataset.py index 56d2d86f1..1b3e77831 100644 --- a/OTAnalytics/domain/track_dataset.py +++ b/OTAnalytics/domain/track_dataset.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass +from datetime import datetime from typing import Callable, Iterable, Iterator, Optional, Sequence from OTAnalytics.domain.event import Event @@ -17,6 +18,16 @@ class TrackDataset(ABC): def __iter__(self) -> Iterator[Track]: yield from self.as_list() + @property + @abstractmethod + def first_occurrence(self) -> datetime: + raise NotImplementedError + + @property + @abstractmethod + def last_occurrence(self) -> datetime: + raise NotImplementedError + @abstractmethod def add_all(self, other: Iterable[Track]) -> "TrackDataset": raise NotImplementedError diff --git a/OTAnalytics/plugin_datastore/python_track_store.py b/OTAnalytics/plugin_datastore/python_track_store.py index 66b3b0ec8..721bcb877 100644 --- a/OTAnalytics/plugin_datastore/python_track_store.py +++ b/OTAnalytics/plugin_datastore/python_track_store.py @@ -233,6 +233,14 @@ def calculate(self, detections: list[Detection]) -> str: class PythonTrackDataset(TrackDataset): """Pure Python implementation of a TrackDataset.""" + @property + def first_occurrence(self) -> datetime: + raise NotImplementedError + + @property + def last_occurrence(self) -> datetime: + raise NotImplementedError + def __init__( self, values: Optional[dict[TrackId, Track]] = None, diff --git a/OTAnalytics/plugin_datastore/track_store.py b/OTAnalytics/plugin_datastore/track_store.py index a1b80c459..ce95b9d01 100644 --- a/OTAnalytics/plugin_datastore/track_store.py +++ b/OTAnalytics/plugin_datastore/track_store.py @@ -197,6 +197,14 @@ def extract_hostname(name: str) -> str: class PandasTrackDataset(TrackDataset): + @property + def first_occurrence(self) -> datetime: + raise NotImplementedError + + @property + def last_occurrence(self) -> datetime: + raise NotImplementedError + def __init__( self, track_geometry_factory: TRACK_GEOMETRY_FACTORY, From 7ddd581138c8181b4490649c3a7fdaa9303d974c Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 09:39:03 +0100 Subject: [PATCH 02/16] Implement first and last occurrence properties for PythonTrackDataset --- OTAnalytics/plugin_datastore/python_track_store.py | 6 ++++-- .../plugin_datastore/test_python_track_storage.py | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/OTAnalytics/plugin_datastore/python_track_store.py b/OTAnalytics/plugin_datastore/python_track_store.py index 721bcb877..eb2e22e58 100644 --- a/OTAnalytics/plugin_datastore/python_track_store.py +++ b/OTAnalytics/plugin_datastore/python_track_store.py @@ -235,11 +235,13 @@ class PythonTrackDataset(TrackDataset): @property def first_occurrence(self) -> datetime: - raise NotImplementedError + return min( + [track.first_detection.occurrence for track in self._tracks.values()] + ) @property def last_occurrence(self) -> datetime: - raise NotImplementedError + return max([track.last_detection.occurrence for track in self._tracks.values()]) def __init__( self, diff --git a/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py b/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py index e4a93f017..d02cd1f56 100644 --- a/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py +++ b/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py @@ -481,3 +481,12 @@ def __create_leave_scene_event(self, track: Track) -> Event: ), video_name=track.last_detection.video_name, ) + + def test_first_occurrence(self, first_track: Track, second_track: Track) -> None: + dataset = PythonTrackDataset.from_list([second_track, first_track]) + assert dataset.first_occurrence == first_track.first_detection.occurrence + assert dataset.first_occurrence == second_track.first_detection.occurrence + + def test_last_occurrence(self, first_track: Track, second_track: Track) -> None: + dataset = PythonTrackDataset.from_list([second_track, first_track]) + assert dataset.last_occurrence == second_track.last_detection.occurrence From 4b4dfaddc8319ca7ed73c399411116b492ee690a Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:01:06 +0100 Subject: [PATCH 03/16] Return None if TrackDataset is empty for first and last occurrence --- OTAnalytics/domain/track_dataset.py | 4 ++-- OTAnalytics/plugin_datastore/python_track_store.py | 8 ++++++-- OTAnalytics/plugin_datastore/track_store.py | 4 ++-- .../plugin_datastore/test_python_track_storage.py | 8 ++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/OTAnalytics/domain/track_dataset.py b/OTAnalytics/domain/track_dataset.py index 1b3e77831..55403f47c 100644 --- a/OTAnalytics/domain/track_dataset.py +++ b/OTAnalytics/domain/track_dataset.py @@ -20,12 +20,12 @@ def __iter__(self) -> Iterator[Track]: @property @abstractmethod - def first_occurrence(self) -> datetime: + def first_occurrence(self) -> datetime | None: raise NotImplementedError @property @abstractmethod - def last_occurrence(self) -> datetime: + def last_occurrence(self) -> datetime | None: raise NotImplementedError @abstractmethod diff --git a/OTAnalytics/plugin_datastore/python_track_store.py b/OTAnalytics/plugin_datastore/python_track_store.py index eb2e22e58..74474c86b 100644 --- a/OTAnalytics/plugin_datastore/python_track_store.py +++ b/OTAnalytics/plugin_datastore/python_track_store.py @@ -234,13 +234,17 @@ class PythonTrackDataset(TrackDataset): """Pure Python implementation of a TrackDataset.""" @property - def first_occurrence(self) -> datetime: + def first_occurrence(self) -> datetime | None: + if not len(self): + return None return min( [track.first_detection.occurrence for track in self._tracks.values()] ) @property - def last_occurrence(self) -> datetime: + def last_occurrence(self) -> datetime | None: + if not len(self): + return None return max([track.last_detection.occurrence for track in self._tracks.values()]) def __init__( diff --git a/OTAnalytics/plugin_datastore/track_store.py b/OTAnalytics/plugin_datastore/track_store.py index ce95b9d01..d6bdcc3da 100644 --- a/OTAnalytics/plugin_datastore/track_store.py +++ b/OTAnalytics/plugin_datastore/track_store.py @@ -198,11 +198,11 @@ def extract_hostname(name: str) -> str: class PandasTrackDataset(TrackDataset): @property - def first_occurrence(self) -> datetime: + def first_occurrence(self) -> datetime | None: raise NotImplementedError @property - def last_occurrence(self) -> datetime: + def last_occurrence(self) -> datetime | None: raise NotImplementedError def __init__( diff --git a/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py b/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py index d02cd1f56..f6fd7ccdd 100644 --- a/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py +++ b/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py @@ -490,3 +490,11 @@ def test_first_occurrence(self, first_track: Track, second_track: Track) -> None def test_last_occurrence(self, first_track: Track, second_track: Track) -> None: dataset = PythonTrackDataset.from_list([second_track, first_track]) assert dataset.last_occurrence == second_track.last_detection.occurrence + + def test_first_occurrence_on_empty_dataset(self) -> None: + dataset = PythonTrackDataset() + assert dataset.first_occurrence is None + + def test_last_occurrence_on_empty_dataset(self) -> None: + dataset = PythonTrackDataset() + assert dataset.last_occurrence is None From 8f3557c6e79715fd20676e463def87229fa03cdb Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:24:35 +0100 Subject: [PATCH 04/16] Implement first and last occurrence for PandasTrackDataset --- OTAnalytics/plugin_datastore/track_store.py | 20 ++++++----- .../plugin_datastore/test_track_store.py | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/OTAnalytics/plugin_datastore/track_store.py b/OTAnalytics/plugin_datastore/track_store.py index d6bdcc3da..6712e4f94 100644 --- a/OTAnalytics/plugin_datastore/track_store.py +++ b/OTAnalytics/plugin_datastore/track_store.py @@ -197,14 +197,6 @@ def extract_hostname(name: str) -> str: class PandasTrackDataset(TrackDataset): - @property - def first_occurrence(self) -> datetime | None: - raise NotImplementedError - - @property - def last_occurrence(self) -> datetime | None: - raise NotImplementedError - def __init__( self, track_geometry_factory: TRACK_GEOMETRY_FACTORY, @@ -227,6 +219,18 @@ def __init__( else: self._geometry_datasets = geometry_datasets + @property + def first_occurrence(self) -> datetime | None: + if not len(self): + return None + return self._dataset.index.get_level_values(LEVEL_OCCURRENCE).min() + + @property + def last_occurrence(self) -> datetime | None: + if not len(self): + return None + return self._dataset.index.get_level_values(LEVEL_OCCURRENCE).max() + @staticmethod def from_list( tracks: list[Track], diff --git a/tests/OTAnalytics/plugin_datastore/test_track_store.py b/tests/OTAnalytics/plugin_datastore/test_track_store.py index 0a6bdc2ea..be1c5ecb6 100644 --- a/tests/OTAnalytics/plugin_datastore/test_track_store.py +++ b/tests/OTAnalytics/plugin_datastore/test_track_store.py @@ -460,3 +460,38 @@ def __create_leave_scene_event(self, track: Track) -> Event: ), video_name=track.last_detection.video_name, ) + + def test_first_occurrence( + self, + track_geometry_factory: TRACK_GEOMETRY_FACTORY, + first_track: Track, + second_track: Track, + ) -> None: + dataset = PandasTrackDataset.from_list( + [first_track, second_track], track_geometry_factory + ) + assert dataset.first_occurrence == first_track.first_detection.occurrence + assert dataset.first_occurrence == second_track.first_detection.occurrence + + def test_last_occurrence( + self, + track_geometry_factory: TRACK_GEOMETRY_FACTORY, + first_track: Track, + second_track: Track, + ) -> None: + dataset = PandasTrackDataset.from_list( + [first_track, second_track], track_geometry_factory + ) + assert dataset.last_occurrence == second_track.last_detection.occurrence + + def test_first_occurrence_on_empty_dataset( + self, track_geometry_factory: TRACK_GEOMETRY_FACTORY + ) -> None: + dataset = PandasTrackDataset(track_geometry_factory) + assert dataset.first_occurrence is None + + def test_last_occurrence_on_empty_dataset( + self, track_geometry_factory: TRACK_GEOMETRY_FACTORY + ) -> None: + dataset = PandasTrackDataset(track_geometry_factory) + assert dataset.last_occurrence is None From 44bd0e1894e0a9bf9d998f1cc7be7a6e080ef5ba Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:28:58 +0100 Subject: [PATCH 05/16] Reorder methods --- OTAnalytics/plugin_datastore/track_store.py | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/OTAnalytics/plugin_datastore/track_store.py b/OTAnalytics/plugin_datastore/track_store.py index 6712e4f94..84b1859b0 100644 --- a/OTAnalytics/plugin_datastore/track_store.py +++ b/OTAnalytics/plugin_datastore/track_store.py @@ -197,6 +197,18 @@ def extract_hostname(name: str) -> str: class PandasTrackDataset(TrackDataset): + @property + def first_occurrence(self) -> datetime | None: + if not len(self): + return None + return self._dataset.index.get_level_values(LEVEL_OCCURRENCE).min() + + @property + def last_occurrence(self) -> datetime | None: + if not len(self): + return None + return self._dataset.index.get_level_values(LEVEL_OCCURRENCE).max() + def __init__( self, track_geometry_factory: TRACK_GEOMETRY_FACTORY, @@ -219,18 +231,6 @@ def __init__( else: self._geometry_datasets = geometry_datasets - @property - def first_occurrence(self) -> datetime | None: - if not len(self): - return None - return self._dataset.index.get_level_values(LEVEL_OCCURRENCE).min() - - @property - def last_occurrence(self) -> datetime | None: - if not len(self): - return None - return self._dataset.index.get_level_values(LEVEL_OCCURRENCE).max() - @staticmethod def from_list( tracks: list[Track], From 759ffcab296e10df034d79ab51bfb3a9bb5d47bc Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:30:52 +0100 Subject: [PATCH 06/16] Define property classifications for TrackDataset --- OTAnalytics/domain/track_dataset.py | 5 +++++ OTAnalytics/plugin_datastore/python_track_store.py | 4 ++++ OTAnalytics/plugin_datastore/track_store.py | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/OTAnalytics/domain/track_dataset.py b/OTAnalytics/domain/track_dataset.py index 55403f47c..40987c961 100644 --- a/OTAnalytics/domain/track_dataset.py +++ b/OTAnalytics/domain/track_dataset.py @@ -28,6 +28,11 @@ def first_occurrence(self) -> datetime | None: def last_occurrence(self) -> datetime | None: raise NotImplementedError + @property + @abstractmethod + def classifications(self) -> frozenset[str]: + raise NotImplementedError + @abstractmethod def add_all(self, other: Iterable[Track]) -> "TrackDataset": raise NotImplementedError diff --git a/OTAnalytics/plugin_datastore/python_track_store.py b/OTAnalytics/plugin_datastore/python_track_store.py index 74474c86b..6b3e43942 100644 --- a/OTAnalytics/plugin_datastore/python_track_store.py +++ b/OTAnalytics/plugin_datastore/python_track_store.py @@ -247,6 +247,10 @@ def last_occurrence(self) -> datetime | None: return None return max([track.last_detection.occurrence for track in self._tracks.values()]) + @property + def classifications(self) -> frozenset[str]: + raise NotImplementedError + def __init__( self, values: Optional[dict[TrackId, Track]] = None, diff --git a/OTAnalytics/plugin_datastore/track_store.py b/OTAnalytics/plugin_datastore/track_store.py index 84b1859b0..654498666 100644 --- a/OTAnalytics/plugin_datastore/track_store.py +++ b/OTAnalytics/plugin_datastore/track_store.py @@ -209,6 +209,10 @@ def last_occurrence(self) -> datetime | None: return None return self._dataset.index.get_level_values(LEVEL_OCCURRENCE).max() + @property + def classifications(self) -> frozenset[str]: + raise NotImplementedError + def __init__( self, track_geometry_factory: TRACK_GEOMETRY_FACTORY, From 31e33d9e49a32856b09dae9931c5364877451cfa Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:39:56 +0100 Subject: [PATCH 07/16] Add unit tests for classifications property of TrackDataset --- .../test_python_track_storage.py | 10 ++++++++++ .../plugin_datastore/test_track_store.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py b/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py index f6fd7ccdd..954654e0d 100644 --- a/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py +++ b/tests/OTAnalytics/plugin_datastore/test_python_track_storage.py @@ -498,3 +498,13 @@ def test_first_occurrence_on_empty_dataset(self) -> None: def test_last_occurrence_on_empty_dataset(self) -> None: dataset = PythonTrackDataset() assert dataset.last_occurrence is None + + def test_classifications(self, first_track: Track, second_track: Track) -> None: + dataset = PythonTrackDataset.from_list([first_track, second_track]) + assert dataset.classifications == frozenset( + [first_track.classification, second_track.classification] + ) + + def test_classifications_on_empty_dataset(self) -> None: + dataset = PythonTrackDataset() + assert dataset.classifications == frozenset() diff --git a/tests/OTAnalytics/plugin_datastore/test_track_store.py b/tests/OTAnalytics/plugin_datastore/test_track_store.py index be1c5ecb6..aeae9f7d4 100644 --- a/tests/OTAnalytics/plugin_datastore/test_track_store.py +++ b/tests/OTAnalytics/plugin_datastore/test_track_store.py @@ -495,3 +495,22 @@ def test_last_occurrence_on_empty_dataset( ) -> None: dataset = PandasTrackDataset(track_geometry_factory) assert dataset.last_occurrence is None + + def test_classifications( + self, + track_geometry_factory: TRACK_GEOMETRY_FACTORY, + first_track: Track, + second_track: Track, + ) -> None: + dataset = PandasTrackDataset.from_list( + [first_track, second_track], track_geometry_factory + ) + assert dataset.classifications == frozenset( + [first_track.classification, second_track.classification] + ) + + def test_classifications_on_empty_dataset( + self, track_geometry_factory: TRACK_GEOMETRY_FACTORY + ) -> None: + dataset = PandasTrackDataset(track_geometry_factory) + assert dataset.classifications == frozenset() From 2ca3ddecb602fe4a2253f785f12556d769121e81 Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:46:28 +0100 Subject: [PATCH 08/16] Implement property classifications for TrackDataset --- OTAnalytics/plugin_datastore/python_track_store.py | 2 +- OTAnalytics/plugin_datastore/track_store.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/OTAnalytics/plugin_datastore/python_track_store.py b/OTAnalytics/plugin_datastore/python_track_store.py index 6b3e43942..e97a58b74 100644 --- a/OTAnalytics/plugin_datastore/python_track_store.py +++ b/OTAnalytics/plugin_datastore/python_track_store.py @@ -249,7 +249,7 @@ def last_occurrence(self) -> datetime | None: @property def classifications(self) -> frozenset[str]: - raise NotImplementedError + return frozenset([track.classification for track in self._tracks.values()]) def __init__( self, diff --git a/OTAnalytics/plugin_datastore/track_store.py b/OTAnalytics/plugin_datastore/track_store.py index 654498666..ce8c137dd 100644 --- a/OTAnalytics/plugin_datastore/track_store.py +++ b/OTAnalytics/plugin_datastore/track_store.py @@ -211,7 +211,9 @@ def last_occurrence(self) -> datetime | None: @property def classifications(self) -> frozenset[str]: - raise NotImplementedError + if not len(self): + return frozenset() + return frozenset(self._dataset[track.TRACK_CLASSIFICATION].unique()) def __init__( self, From 1ece2b0567eb9173fece7d62db25e1756439967f Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 11:13:33 +0100 Subject: [PATCH 09/16] Extend TrackRepository with new properties --- OTAnalytics/domain/track_repository.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/OTAnalytics/domain/track_repository.py b/OTAnalytics/domain/track_repository.py index 0e9b74a59..4338f873e 100644 --- a/OTAnalytics/domain/track_repository.py +++ b/OTAnalytics/domain/track_repository.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass +from datetime import datetime from pathlib import Path from typing import Iterable, Optional @@ -99,6 +100,18 @@ def __init__(self, track_ids: list[TrackId], message: str): class TrackRepository: + @property + def first_occurrence(self) -> datetime | None: + return self._dataset.first_occurrence + + @property + def last_occurrence(self) -> datetime | None: + return self._dataset.first_occurrence + + @property + def classifications(self) -> frozenset[str]: + return self._dataset.classifications + def __init__(self, dataset: TrackDataset) -> None: self._dataset = dataset self.observers = Subject[TrackRepositoryEvent]() From 13ac5cceab7fce649611774f681eb26d01513666 Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:05:20 +0100 Subject: [PATCH 10/16] Use classification property of TrackRepository in TracksMetadata --- OTAnalytics/application/state.py | 18 +++++++---------- .../track_visualization/track_viz.py | 2 +- tests/OTAnalytics/application/test_state.py | 20 +++++-------------- .../track_visualization/test_track_viz.py | 2 +- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/OTAnalytics/application/state.py b/OTAnalytics/application/state.py index 5cdff1bde..8a93d2b6f 100644 --- a/OTAnalytics/application/state.py +++ b/OTAnalytics/application/state.py @@ -436,9 +436,9 @@ def __init__(self, track_repository: TrackRepository) -> None: self._last_detection_occurrence: ObservableOptionalProperty[ datetime ] = ObservableOptionalProperty[datetime]() - self._classifications: ObservableProperty[set[str]] = ObservableProperty[set]( - set() - ) + self._classifications: ObservableProperty[frozenset[str]] = ObservableProperty[ + frozenset + ](frozenset()) self._detection_classifications: ObservableProperty[ frozenset[str] ] = ObservableProperty[frozenset](frozenset([])) @@ -464,7 +464,7 @@ def last_detection_occurrence(self) -> Optional[datetime]: return self._last_detection_occurrence.get() @property - def classifications(self) -> set[str]: + def classifications(self) -> frozenset[str]: """The current classifications in the track repository. Returns: @@ -484,7 +484,7 @@ def detection_classifications(self) -> frozenset[str]: def notify_tracks(self, track_event: TrackRepositoryEvent) -> None: """Update tracks metadata on track repository changes""" self._update_detection_occurrences() - self._update_classifications(track_event.added) + self._update_classifications() def _update_detection_occurrences(self) -> None: """Update the first and last detection occurrences.""" @@ -495,13 +495,9 @@ def _update_detection_occurrences(self) -> None: self._first_detection_occurrence.set(sorted_detections[0].occurrence) self._last_detection_occurrence.set(sorted_detections[-1].occurrence) - def _update_classifications(self, new_tracks: list[TrackId]) -> None: + def _update_classifications(self) -> None: """Update current classifications.""" - updated_classifications = self._classifications.get().copy() - for track_id in new_tracks: - if track := self._track_repository.get_for(track_id): - updated_classifications.add(track.classification) - self._classifications.set(updated_classifications) + self._classifications.set(self._track_repository.classifications) def _get_all_track_detections(self) -> Iterable[Detection]: """Get all track detections in the track repository. diff --git a/OTAnalytics/plugin_prototypes/track_visualization/track_viz.py b/OTAnalytics/plugin_prototypes/track_visualization/track_viz.py index 5b45b5dc7..4e9f82601 100644 --- a/OTAnalytics/plugin_prototypes/track_visualization/track_viz.py +++ b/OTAnalytics/plugin_prototypes/track_visualization/track_viz.py @@ -120,7 +120,7 @@ def __init__(self, default_palette: dict[str, str]) -> None: self._default_palette = default_palette self._palette: dict[str, str] = {} - def update(self, classifications: set[str]) -> None: + def update(self, classifications: frozenset[str]) -> None: for classification in classifications: if classification in self._default_palette.keys(): self._palette[classification] = self._default_palette[classification] diff --git a/tests/OTAnalytics/application/test_state.py b/tests/OTAnalytics/application/test_state.py index c3abfa4f0..1d2c2ccb9 100644 --- a/tests/OTAnalytics/application/test_state.py +++ b/tests/OTAnalytics/application/test_state.py @@ -382,26 +382,16 @@ def test_get_all_track_detections( assert detections == [first_detection, second_detection] track_repository.get_all.assert_called_once() - def test_update_classifications(self, track: Mock) -> None: + def test_update_classifications(self) -> None: + classifications = frozenset(["truck", "car", "pedestrian"]) mock_track_repository = Mock(spec=TrackRepository) - mock_track_repository.get_for.return_value = track + mock_track_repository.classifications = classifications tracks_metadata = TracksMetadata(mock_track_repository) - assert tracks_metadata.classifications == set() - tracks_metadata._update_classifications([track.id]) - - assert tracks_metadata.classifications == {"car"} - mock_track_repository.get_for.assert_any_call(track.id) - assert mock_track_repository.get_for.call_count == 1 - - track.detections[0].classification = "bicycle" - tracks_metadata._update_classifications([track.id]) - - assert tracks_metadata.classifications == {"car"} - mock_track_repository.get_for.assert_any_call(track.id) - assert mock_track_repository.get_for.call_count == 2 + tracks_metadata._update_classifications() + assert tracks_metadata.classifications == classifications def test_update_detection_classes(self) -> None: tracks_metadata = TracksMetadata(Mock()) diff --git a/tests/OTAnalytics/plugin_prototypes/track_visualization/test_track_viz.py b/tests/OTAnalytics/plugin_prototypes/track_visualization/test_track_viz.py index 605214efd..d179cdb55 100644 --- a/tests/OTAnalytics/plugin_prototypes/track_visualization/test_track_viz.py +++ b/tests/OTAnalytics/plugin_prototypes/track_visualization/test_track_viz.py @@ -306,7 +306,7 @@ class TestColorPaletteProvider: ) def test_update_with_filled_default( self, - new_classifications: set[str], + new_classifications: frozenset[str], default_palette: dict[str, str], expected: dict[str, str], ) -> None: From dc54d870f474db5b9b6ab551cf6d524a98feeb60 Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:16:53 +0100 Subject: [PATCH 11/16] Use first and last occurrence properties of TrackRepository in TracksMetadata --- OTAnalytics/application/state.py | 25 ++---------- tests/OTAnalytics/application/test_state.py | 44 +++++---------------- 2 files changed, 14 insertions(+), 55 deletions(-) diff --git a/OTAnalytics/application/state.py b/OTAnalytics/application/state.py index 8a93d2b6f..8d938895f 100644 --- a/OTAnalytics/application/state.py +++ b/OTAnalytics/application/state.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from datetime import datetime -from typing import Callable, Generic, Iterable, Optional +from typing import Callable, Generic, Optional from OTAnalytics.application.config import DEFAULT_TRACK_OFFSET from OTAnalytics.application.datastore import Datastore, VideoMetadata @@ -17,7 +17,7 @@ SectionRepositoryEvent, SectionType, ) -from OTAnalytics.domain.track import Detection, TrackId, TrackImage +from OTAnalytics.domain.track import TrackId, TrackImage from OTAnalytics.domain.track_repository import ( TrackListObserver, TrackObserver, @@ -488,30 +488,13 @@ def notify_tracks(self, track_event: TrackRepositoryEvent) -> None: def _update_detection_occurrences(self) -> None: """Update the first and last detection occurrences.""" - sorted_detections = sorted( - self._get_all_track_detections(), key=lambda x: x.occurrence - ) - if sorted_detections: - self._first_detection_occurrence.set(sorted_detections[0].occurrence) - self._last_detection_occurrence.set(sorted_detections[-1].occurrence) + self._first_detection_occurrence.set(self._track_repository.first_occurrence) + self._last_detection_occurrence.set(self._track_repository.last_occurrence) def _update_classifications(self) -> None: """Update current classifications.""" self._classifications.set(self._track_repository.classifications) - def _get_all_track_detections(self) -> Iterable[Detection]: - """Get all track detections in the track repository. - - Returns: - Iterable[Detection]: the track detections. - """ - detections: list[Detection] = [] - - for track in self._track_repository.get_all(): - detections.extend(track.detections) - - return detections - def update_detection_classes(self, new_classes: frozenset[str]) -> None: """Update the classifications used by the detection model.""" updated_classes = self._detection_classifications.get().union(new_classes) diff --git a/tests/OTAnalytics/application/test_state.py b/tests/OTAnalytics/application/test_state.py index 1d2c2ccb9..7bf333c98 100644 --- a/tests/OTAnalytics/application/test_state.py +++ b/tests/OTAnalytics/application/test_state.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta, timezone from typing import Callable, Optional -from unittest.mock import Mock, call, patch +from unittest.mock import Mock, call import pytest @@ -342,45 +342,21 @@ def track( track.detections = [first_detection, second_detection, third_detection] return track - @patch("OTAnalytics.application.state.TracksMetadata._get_all_track_detections") - def test_update_detection_occurrences( - self, - mock_get_all_track_detections: Mock, - first_detection: Mock, - second_detection: Mock, - third_detection: Mock, - ) -> None: - mock_track_repository = Mock(spec=TrackRepository) + def test_update_detection_occurrences(self) -> None: + first_occurrence = datetime(2000, 1, 1, 12) + last_occurrence = datetime(2000, 1, 1, 15) + track_repository = Mock(spec=TrackRepository) + track_repository.first_occurrence = first_occurrence + track_repository.last_occurrence = last_occurrence - mock_get_all_track_detections.return_value = [ - first_detection, - third_detection, - second_detection, - ] - tracks_metadata = TracksMetadata(mock_track_repository) + tracks_metadata = TracksMetadata(track_repository) assert tracks_metadata.first_detection_occurrence is None assert tracks_metadata.last_detection_occurrence is None tracks_metadata._update_detection_occurrences() - assert tracks_metadata.first_detection_occurrence == first_detection.occurrence - assert tracks_metadata.last_detection_occurrence == third_detection.occurrence - - mock_get_all_track_detections.assert_called_once() - - def test_get_all_track_detections( - self, first_detection: Mock, second_detection: Mock - ) -> None: - track = Mock(spec=Track).return_value - track.detections = [first_detection, second_detection] - track_repository = Mock(spec=TrackRepository) - track_repository.get_all.return_value = [track] - - tracks_metadata = TracksMetadata(track_repository) - detections = tracks_metadata._get_all_track_detections() - - assert detections == [first_detection, second_detection] - track_repository.get_all.assert_called_once() + assert tracks_metadata.first_detection_occurrence == first_occurrence + assert tracks_metadata.last_detection_occurrence == last_occurrence def test_update_classifications(self) -> None: classifications = frozenset(["truck", "car", "pedestrian"]) From d86610aa30e301d8a83c42b1345e0b5643064ca7 Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:28:21 +0100 Subject: [PATCH 12/16] Fix selecting wrong property for last occurrence in TrackRepository --- OTAnalytics/domain/track_repository.py | 2 +- tests/OTAnalytics/domain/test_track_repository.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/OTAnalytics/domain/track_repository.py b/OTAnalytics/domain/track_repository.py index 4338f873e..38b9091c5 100644 --- a/OTAnalytics/domain/track_repository.py +++ b/OTAnalytics/domain/track_repository.py @@ -106,7 +106,7 @@ def first_occurrence(self) -> datetime | None: @property def last_occurrence(self) -> datetime | None: - return self._dataset.first_occurrence + return self._dataset.last_occurrence @property def classifications(self) -> frozenset[str]: diff --git a/tests/OTAnalytics/domain/test_track_repository.py b/tests/OTAnalytics/domain/test_track_repository.py index 4a2a3daaa..71eeeee91 100644 --- a/tests/OTAnalytics/domain/test_track_repository.py +++ b/tests/OTAnalytics/domain/test_track_repository.py @@ -146,6 +146,20 @@ def test_len(self) -> None: assert result == expected_size dataset.__len__.assert_called_once() + def test_first_occurrence(self) -> None: + first_occurrence = Mock() + dataset = Mock() + dataset.first_occurrence = first_occurrence + repository = TrackRepository(dataset) + assert repository.first_occurrence == first_occurrence + + def test_last_occurrence(self) -> None: + last_occurrence = Mock() + dataset = Mock() + dataset.last_occurrence = last_occurrence + repository = TrackRepository(dataset) + assert repository.last_occurrence == last_occurrence + class TestTrackFileRepository: @pytest.fixture From 32db5915f809c68d194df25a29b7e650ab509ba1 Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:29:17 +0100 Subject: [PATCH 13/16] Add unit test for classification property in TrackRepository --- tests/OTAnalytics/domain/test_track_repository.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/OTAnalytics/domain/test_track_repository.py b/tests/OTAnalytics/domain/test_track_repository.py index 71eeeee91..37b68008b 100644 --- a/tests/OTAnalytics/domain/test_track_repository.py +++ b/tests/OTAnalytics/domain/test_track_repository.py @@ -160,6 +160,13 @@ def test_last_occurrence(self) -> None: repository = TrackRepository(dataset) assert repository.last_occurrence == last_occurrence + def test_classifications(self) -> None: + classifications = Mock() + dataset = Mock() + dataset.classifications = classifications + repository = TrackRepository(dataset) + assert repository.classifications == classifications + class TestTrackFileRepository: @pytest.fixture From 27ffe21108d0768bc5ec2e168fcb2b00d5de433f Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:38:43 +0100 Subject: [PATCH 14/16] Fix typo --- OTAnalytics/application/track_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OTAnalytics/application/track_filter.py b/OTAnalytics/application/track_filter.py index 446665a68..f32b5c758 100644 --- a/OTAnalytics/application/track_filter.py +++ b/OTAnalytics/application/track_filter.py @@ -83,7 +83,7 @@ def test(self, to_test: Track) -> bool: to_test (Track): the track under test Returns: - bool: `True` if track has classification. Otherwise `False`. + bool: `True` if track has classification. Otherwise, `False`. """ return to_test.classification in self._classifications From f06e7412d6b13fbd009a4adcc91058ac217d248e Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:39:40 +0100 Subject: [PATCH 15/16] Clean up documentation --- OTAnalytics/application/track_filter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/OTAnalytics/application/track_filter.py b/OTAnalytics/application/track_filter.py index f32b5c758..d50505d1d 100644 --- a/OTAnalytics/application/track_filter.py +++ b/OTAnalytics/application/track_filter.py @@ -92,7 +92,6 @@ class TrackFilter(Filter[Track, bool]): """A `Track` filter. Args: - Filter (Filter[Track, bool]): extends the `Filter` interface predicate (Predicate[Track, bool]): the predicate to test against during filtering """ From f09af72cbfd28c44e9ac61dd611dabd7442935df Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:21:57 +0100 Subject: [PATCH 16/16] Use track properties --- OTAnalytics/application/track_filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OTAnalytics/application/track_filter.py b/OTAnalytics/application/track_filter.py index d50505d1d..28454bb44 100644 --- a/OTAnalytics/application/track_filter.py +++ b/OTAnalytics/application/track_filter.py @@ -46,7 +46,7 @@ def __init__(self, start_date: datetime) -> None: self._start_date = start_date def test(self, to_test: Track) -> bool: - return self._start_date <= to_test.detections[0].occurrence + return self._start_date <= to_test.first_detection.occurrence class TrackEndsBeforeOrAtDate(TrackPredicate): @@ -60,7 +60,7 @@ def __init__(self, end_date: datetime) -> None: self._end_date = end_date def test(self, to_test: Track) -> bool: - return to_test.detections[0].occurrence <= self._end_date + return to_test.first_detection.occurrence <= self._end_date class TrackHasClassifications(TrackPredicate):