import copy
import uuid
from typing import MutableSequence, MutableMapping
from .multihypothesis import MultipleHypothesis
from .state import State, StateMutableSequence
from .update import Update
from ..base import Property
[docs]
class Track(StateMutableSequence):
"""Track type
A :class:`~.StateMutableSequence` representing a track.
Notes:
Any manual modifications to :attr:`metadata` or :attr:`metadatas` will be overwritten if a
state is inserted at a point prior to where the modifications are made.
For example, inserting a state at the start of :attr:`states` will result in a
:attr:`metadatas` update that will update all subsequent metadata values, resulting in
manual metadata modifications being lost.
"""
states: MutableSequence[State] = Property(
default=None,
doc="The initial states of the track. Default `None` which initialises with empty list.")
id: str = Property(default=None, doc="The unique track ID")
init_metadata: MutableMapping = Property(
default={}, doc="Initial dictionary of metadata items for track. Default `None` which "
"initialises track metadata as an empty dictionary.")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.metadatas = list()
for state in self.states:
self._update_metadata_from_state(state)
if self.id is None:
self.id = str(uuid.uuid4())
def __setitem__(self, index, value):
super().__setitem__(index, value)
if index < 0:
index = len(self.states) + index
self._update_metadatas(index)
def __copy__(self):
inst = super().__copy__()
inst.__dict__['metadatas'] = copy.copy(self.__dict__['metadatas'])
return inst
[docs]
def insert(self, index, value):
"""Insert value at index of :attr:`states`.
Parameters
----------
index: int
Index of :attr:`states` to insert value at.
value: State
A state object to be inserted at the specified index of :attr:`states`.
"""
super().insert(index, value)
if index < 0:
if index < -len(self.states):
index = 0
else:
index += len(self.states) - 1
elif index >= len(self.states):
index = len(self.states) - 1
self._update_metadatas(index)
[docs]
def append(self, value):
"""Add value at end of :attr:`states`.
Parameters
----------
value: State
A state object to be added at the end of :attr:`states`.
"""
# Update metadata
self._update_metadata_from_state(value)
return self.states.append(value)
@property
def metadata(self):
"""Current metadata dictionary of track. If track contains no states, this is the initial
metadata dictionary :attr:`init_metadata`."""
if self.metadatas:
return self.metadatas[-1]
else:
return self.init_metadata
def _update_metadatas(self, index):
"""Update track :attr:`metadatas` property, starting at specified index.
Parameters
----------
index: Int
Index of :attr:`metadatas` to update from.
"""
# Plus one for 0th initial track meta data
self.metadatas = self.metadatas[:index]
for future_state in self.states[index:]:
self._update_metadata_from_state(future_state)
def _update_metadata_from_state(self, state):
"""Update :attr:`metadatas` with an updated metadata entry, accounting for extracted
metadata from state.
Parameters
----------
state: State
A state object from which to extract metadata. Metadata can only be extracted from
Update (or subclassed) objects. Calling this method with a non-Update (subclass) object
will NOT raise an error, but will have no effect on the metadata.
"""
self.metadatas.append(self.metadata.copy())
if isinstance(state, Update):
if isinstance(state.hypothesis, MultipleHypothesis):
# Sort and iterate through multiple hypotheses such that most
# likely hypothesis comes last. This ensures that metadata
# from all hypotheses are retained, but more likely
# hypotheses will over-write the metadata set by less likely
# ones.
try:
for hypothesis in sorted(state.hypothesis, reverse=True):
if hypothesis \
and hypothesis.measurement.metadata is not None:
self.metadata.update(hypothesis.measurement.metadata)
except TypeError:
pass
else:
hypothesis = state.hypothesis
if hypothesis and hypothesis.measurement.metadata is not None:
self.metadata.update(hypothesis.measurement.metadata)