Skip to content

Commit

Permalink
Merge pull request #840 from dstl/improve_example_documentation
Browse files Browse the repository at this point in the history
Improve example documentation
  • Loading branch information
sdhiscocks committed Oct 16, 2023
2 parents 1be644c + c85f654 commit 71b47f9
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 333 deletions.
24 changes: 12 additions & 12 deletions docs/examples/Creating_Actionable_Sensor.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
#!/usr/bin/env python

"""
Creating an Actionable Sensor Example
=====================================
Creating an Actionable Sensor
=============================
This example demonstrates the process of creating an actionable sensor, i.e., a sensor that has an
actionable property. This includes creation of :class:`~.Action` and :class:`~.ActionGenerator`
that will handle how this property evolves over time, and how to interface with the sensor via
classes that handle how this property evolves over time, and how to interface with the sensor via
given actions.
"""

# %%
# The Task
# ^^^^^^^^
# We will create a simple sensor with a field of view and infinite range. It lies on the 2D
# Cartesian plane and can look in 1 of 4 directions: East, North, West, South.
# Let's call the attribute of the sensor that takes these values the `direction`, and model it
# Cartesian plane and can look North, East, South, or West.
# Let's call the attribute of the sensor that takes these values the ``direction``, and model it
# such that it can instantaneously switch to a new value.
# We'll define its field of view (FoV) as 90 degrees so that its observation of a particular
# direction leads to it being completely blind of the areas observable in the other 3 directions.

# %%
# Action
# ------
# The logic of this `direction` switching will be handled by our custom :class:`~.Action`.
# The logic of this ``direction`` switching will be handled by our custom :class:`~.Action`.
# The class' :meth:`~.Action.act` method contains the calculations needed to take in the
# property's current value and return its new value after a particular amount of time has elapsed.
#
Expand Down Expand Up @@ -70,7 +70,7 @@ def act(self, current_time, timestamp, init_value):
return init_value # same direction

# %%
# It is within the :class:`~.Action` where you can detail more complicated modifications to the
# :class:`~.Action` is where you can detail more complicated modifications to the
# attribute. For example, the :class:`~.ChangeDwellAction` is an action for use with the
# :attr:`~.RadarRotatingBearingRange.dwell_centre` property of the
# :class:`~.RadarRotatingBearingRange` sensor (or any other model with similar dwell dynamics). It
Expand All @@ -81,7 +81,7 @@ def act(self, current_time, timestamp, init_value):
# %%
# Action Generator
# ----------------
# Now that we have the logic of how the `direction` can change over time, we need to detail what
# Now that we have the logic of how the ``direction`` can change over time, we need to detail what
# the potential changes can be for a given time frame. An :class:`~.ActionGenerator` type handles
# the details of these potential property values.
# In the more complicated dwell centre example above, this might be in determining what potential
Expand All @@ -103,20 +103,20 @@ def act(self, current_time, timestamp, init_value):
#
# By inheriting this class, we are required to define several things:
#
# * the :attr:`~.ActionGenerator.default_action` property determines what the behaviour of the
# * The :attr:`~.ActionGenerator.default_action` property determines what the behaviour of the
# property should be, given no actions have been passed to it (or that it has no actions to
# perform at the given time). For our `direction` example, we'll simply say that the direction
# won't change. So the default action should be one of our :class:`ChangeDirectionAction`
# types with a target value equal to the current value of the direction. For the dwell centre
# example discussed above, this might be reverting to a default, anti-clockwise rotation at the
# given rpm. The default action's end-time should last until the end of the query (i.e. until
# :attr:`~.ActionGenerator.end_time`).
# * the :meth:`__iter__` method defines how we calculate the potential actions for the sensor in
# * The :meth:`__iter__` method defines how we calculate the potential actions for the sensor in
# the given time frame. We should be able to loop through this generator object and get out a
# :class:`ChangeDirectionAction` for every potential new direction.
# * we should also define the :meth:`__contains__` method for this generator.
# * We should also define the :meth:`__contains__` method for this generator.
# This way, for a given :class:`ChangeDirectionAction` or particular direction, we can say
# whether this is possible by simply asking "is this IN my generator?"
# whether this is possible by simply asking "is this IN my generator?".

from stonesoup.sensor.action import ActionGenerator
from stonesoup.base import Property
Expand Down
78 changes: 38 additions & 40 deletions docs/examples/Disjoint_Tracking_Classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"""
Disjoint Tracking and Classification
====================================
This is a demonstration of a utilisation of the implemented Hidden Markov model and composite
tracking modules in order to categorise a target as well as track its kinematics.
This example uses the implemented Hidden Markov model and composite tracking modules to categorise
a target and track its kinematics.
"""

# %%
Expand All @@ -20,8 +20,8 @@
from stonesoup.types.groundtruth import GroundTruthState

# %%
# Ground Truth, Categorical and Composite States
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# Ground Truth, Categorical, and Composite States
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# We will attempt to track and classify 3 targets.

# %%
Expand All @@ -42,7 +42,7 @@
# True Classifications
# --------------------
# A target may take one of three discrete hidden classes: 'bike', 'car' and 'bus'.
# It will be assumed that the targets cannot transition from one class to another, hence an
# It will be assumed that the targets cannot transition from one class to another, so an
# identity transition matrix is given to the :class:`~.MarkovianTransitionModel` for all targets.
#
# A :class:`~.CategoricalState` class is used to store information on the classification/'category'
Expand Down Expand Up @@ -118,7 +118,7 @@
# %%
# Plotting Ground Truths
# ----------------------
# Colour will be used in plotting as an indicator to category: red == 'bike', green == 'car',
# Colour will be used in plotting as an indicator to category: red == 'bike', green == 'car', and
# blue == 'bus'.
fig, axes = plt.subplots(1, 3, figsize=(10, 5))
fig.set_figheight(15)
Expand Down Expand Up @@ -159,22 +159,21 @@ def set_axes_limits():
# %%
# Measurement
# ^^^^^^^^^^^
# A new sensor will be created, that can provide the information needed to both track and classify
# A new sensor will be created that can provide the information needed to both track and classify
# the targets.

# %%
# Composite Detection
# -------------------
# Detections relating to both the kinematics and classification will be needed. Therefore we will
# create a sensor that outputs :class:`~.CompositeDetection` types. The input `sensors` list will
# provide the contents of these compositions. For this example we will provide a
# :class:`~.RadarBearingRange` and a :class:`~.HMMSensor` for kinematics and classification
# respectively.
# :class:`~.CompositeDetection` types have a `mapping` attribute, which defines what sub-state
# index each sub-detection was created from. For example, with a composite state of form:
# (kinematic state, categorical state), and composite detection with mapping (1, 0), this would
# indicate that the 0th index sub-detection was attained from the categorical state, and the 1st
# index sub-detection from the kinematic state.
# Detections relating to both the kinematics and classification of the targets will be needed.
# Therefore, we will create a sensor that outputs :class:`~.CompositeDetection` types. The input
# ``sensors`` list will provide the contents of these compositions. For this example, we will
# provide a :class:`~.RadarBearingRange` and a :class:`~.HMMSensor` for kinematics and
# classification respectively. :class:`~.CompositeDetection` types have a `mapping` attribute
# which defines which sub-state index each sub-detection was created from. For example, a
# composite state of form (kinematic state, categorical state), and composite detection with
# mapping (1, 0), would indicate that the 0th index sub-detection was attained from the
# categorical state, and the 1st index sub-detection from the kinematic state.

from typing import Set, Union, Sequence

Expand Down Expand Up @@ -242,20 +241,21 @@ def measurement_model(self):
# %%
# Categorical Measurement
# -----------------------
# Using the hidden Markov model, it is assumed the hidden class of the target cannot be directly
# observed, and instead indirect observations are taken. In this instance, observations of the
# target's size are taken ('small' or 'large'), which have direct implications as to the target's
# hidden class, and this relationship is modelled by the `emission matrix` of the
# Using the hidden Markov model, it is assumed that the hidden class of the target cannot be
# directly observed, and instead indirect observations are taken. In this instance, observations of
# the target's size are taken ('small' or 'large'), which have direct implications as to the
# target's hidden class. This relationship is modelled by the `emission matrix` of the
# :class:`~.MarkovianMeasurementModel`, which is used by the :class:`~.HMMSensor` to
# provide :class:`~.CategoricalDetection` types.
#
# We will model this such that a 'bike' has a very small chance of being observed as a 'big'
# target. Similarly, a 'bus' will tend to appear as 'large'. Whereas, a 'car' has equal chance of
# being observed as either.
# target. Similarly, a 'bus' will tend to appear as 'large'. A 'car' has equal chance of being
# observed as either.

from stonesoup.models.measurement.categorical import MarkovianMeasurementModel
from stonesoup.sensor.categorical import HMMSensor

E = np.array([[0.99, 0.5, 0.01], # P(small | bike), P(small | car), P(small | bus
E = np.array([[0.99, 0.5, 0.01], # P(small | bike), P(small | car), P(small | bus)
[0.01, 0.5, 0.99]])
model = MarkovianMeasurementModel(emission_matrix=E,
measurement_categories=['small', 'large'])
Expand All @@ -265,7 +265,7 @@ def measurement_model(self):
# %%
# Composite Sensor
# ----------------
# Creating the composite sensor class.
# Here, we create the composite sensor class.

sensor = CompositeSensor(sensors=[eo, radar], mapping=[1, 0])

Expand All @@ -292,7 +292,7 @@ def measurement_model(self):
# %%
# Plotting Measurements
# ---------------------
# Colour will be used to indicate measurement category: orange == 'small', light-blue == 'large'.
# Colour will be used to indicate measurement category: orange == 'small' and light-blue == 'large'.
# It is expected that the bus will have mostly light-blue (large) measurements coinciding with its
# route, the bike will have mostly orange (small), and the car a roughly even split of both.

Expand Down Expand Up @@ -329,8 +329,7 @@ def measurement_model(self):
# Though not used by the tracking components here, a :class:`~.CompositePredictor` will predict
# the component states of a composite state forward, according to a list of sub-predictors.
#
# A :class:`~.HMMPredictor` specifically uses :class:`~.MarkovianTransitionModel` types to
# predict.
# A :class:`~.HMMPredictor` specifically uses :class:`~.MarkovianTransitionModel` types to predict.

from stonesoup.predictor.kalman import KalmanPredictor
from stonesoup.predictor.categorical import HMMPredictor
Expand All @@ -344,8 +343,8 @@ def measurement_model(self):
# %%
# Updater
# -------
# The :class:`~.CompositeUpdater` composite updater will update each component sub-state according
# to a list of corresponding sub-updaters. It has no method to create measurement predictions.
# The :class:`~.CompositeUpdater` will update each component sub-state according to a list of
# corresponding sub-updaters. It has no method to create measurement predictions.
# This is instead handled on instantiation of :class:`~.CompositeHypothesis` types: the expected
# arguments to the updater's `update` method.

Expand All @@ -364,13 +363,13 @@ def measurement_model(self):
# The hypothesiser is a :class:`~.CompositeHypothesiser` type. It is in the data association step
# that tracking and classification are combined: for each measurement, a hypothesis is created for
# both a track's kinematic and categorical components. A :class:`~.CompositeHypothesis` type is
# created, which contains these sub-hypotheses, whereby its weight is equal to the product of the
# created which contains these sub-hypotheses, whereby its weight is equal to the product of the
# sub-hypotheses' weights. These sub-hypotheses should be probabilistic.
#
# The :class:`CompositeHypothesiser` uses a list of sub-hypothesisers to create these
# sub-hypotheses, hence the sub-hypothesisers should also be probabilistic.
# In this example we will define a hypothesiser that simply changes kinematic distance weights in
# to probabilities for hypothesising the kinematic sub-state of the track.
# In this example, we will define a hypothesiser that simply changes kinematic distance weights into
# probabilities for hypothesising the kinematic sub-state of the track.

from stonesoup.measures import Mahalanobis
from stonesoup.hypothesiser.distance import DistanceHypothesiser
Expand All @@ -397,10 +396,9 @@ def hypothesise(self, track, detections, timestamp, **kwargs):
measure=Mahalanobis())

# %%
# A :class:`~.HMMHypothesiser` is used for calculating categorical hypotheses.
# It utilises the :class:`~.ObservationAccuracy` measure: a multi-dimensional extension of an
# 'accuracy' score, essentially providing a measure of the similarity between two categorical
# distributions.
# A :class:`~.HMMHypothesiser` is used for calculating categorical hypotheses. It utilises the
# :class:`~.ObservationAccuracy` measure: a multidimensional extension of an 'accuracy' score,
# essentially providing a measure of the similarity between two categorical distributions.

from stonesoup.hypothesiser.categorical import HMMHypothesiser
from stonesoup.hypothesiser.composite import CompositeHypothesiser
Expand All @@ -424,7 +422,7 @@ def hypothesise(self, track, detections, timestamp, **kwargs):
# Prior
# -----
# As we are tracking in a composite state space, we should initiate tracks with a
# :class:`~.CompositeState` type. The kinematic sub-state of the prior is a usual Gaussian state.
# :class:`~.CompositeState` type. The kinematic sub-state of the prior is a Gaussian state.
# For the categorical sub-state of the prior, equal probability is given to all 3 of the possible
# hidden classes that a target might take (the category names are also provided here).

Expand All @@ -438,7 +436,7 @@ def hypothesise(self, track, detections, timestamp, **kwargs):
# Initiator
# ---------
# The initiator is composite. For each unassociated detection, a new track will be initiated. In
# this instance we use a :class:`~.CompositeUpdateInitiator` type. A detection has both kinematic
# this instance, we use a :class:`~.CompositeUpdateInitiator` type. A detection has both kinematic
# and categorical information to initiate the 2 state space sub-states from. However, in an
# instance where a detection only provides one of these, the missing sub-state for the track will
# be initiated as the given prior's sub-state (eg. if a detection provides only kinematic
Expand Down Expand Up @@ -493,7 +491,7 @@ def hypothesise(self, track, detections, timestamp, **kwargs):
# ---------------
# Colour will be used to indicate a track's hidden category distribution. The `rgb` value is
# defined by the 'bike', 'car', and 'bus' probabilities. For example, a track with high probability
# of being a 'bike' will have a high 'r' value, and hence appear more red.
# of being a 'bike' will have a high 'r' value and will appear more red.

for track in tracks:
for i, state in enumerate(track[1:], 1):
Expand Down
Loading

0 comments on commit 71b47f9

Please sign in to comment.