Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce 2D Obstacles, handling for sensors and the Visibility Informed Bernoulli Filter #1096

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0204ad0
Initial visibility informed changes
timothy-glover Jul 2, 2024
cddf4df
Create VisibilityInformed2DSensor, obstacle platform and modified inh…
timothy-glover Aug 27, 2024
236f3e5
Initial visibility informed changes
timothy-glover Jul 2, 2024
0dfd7f7
Create VisibilityInformed2DSensor, obstacle platform and modified inh…
timothy-glover Aug 27, 2024
5ccd114
Merge local changes after rebasing onto main
timothy-glover Aug 30, 2024
eb9c2cb
Document obstacle and is_visible, add from_obstacle and add vertices …
timothy-glover Oct 6, 2024
2f250bb
Create the visibility informed bernoulli particle filter
timothy-glover Oct 6, 2024
7920796
Add plot obstacles to animated plotterly and plotterly
timothy-glover Oct 6, 2024
15d8da1
Create tests for obstacle platforms and add shape_mapping property to…
timothy-glover Oct 9, 2024
87e8bba
Fix minor bugs, improve functionality, correctly format code and crea…
timothy-glover Oct 23, 2024
d876315
Correct doc string and comment errors
timothy-glover Oct 24, 2024
dba534e
Add doc string for Plotly plot_obstacles and improve consistency of i…
timothy-glover Oct 24, 2024
059b471
Correct plot_obstacles label argument
timothy-glover Oct 25, 2024
b784de3
Complete obstacle label changes
timothy-glover Oct 25, 2024
f095bc9
Fix repeated obstacle figure label for Plotterly and made obstacle la…
timothy-glover Nov 7, 2024
b533cbd
Correct plot_obstacles documentation
jswright-dstl Nov 12, 2024
347c642
Add plot_obstacle method to Plotter and add NotImplementedErrors
jswright-dstl Nov 12, 2024
7bd76fb
Add tests for plot_obstacles
jswright-dstl Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions stonesoup/platform/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from .base import Platform, MovingPlatform, FixedPlatform, MultiTransitionMovingPlatform
from .base import Platform, MovingPlatform, FixedPlatform, MultiTransitionMovingPlatform, Obstacle

__all__ = ['Platform', 'MovingPlatform', 'FixedPlatform', 'MultiTransitionMovingPlatform']

__all__ = ['Platform', 'MovingPlatform', 'FixedPlatform', 'MultiTransitionMovingPlatform',
'Obstacle']
166 changes: 165 additions & 1 deletion stonesoup/platform/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import uuid
from typing import MutableSequence
from typing import MutableSequence, Sequence, Union, Any
import numpy as np
from functools import lru_cache

from stonesoup.base import Property, Base
from stonesoup.functions import build_rotation_matrix
from stonesoup.movable import Movable, FixedMovable, MovingMovable, MultiTransitionMovable
from stonesoup.sensor.sensor import Sensor
from stonesoup.types.array import StateVectors
from stonesoup.types.groundtruth import GroundTruthPath


Expand Down Expand Up @@ -189,3 +193,163 @@ class MovingPlatform(Platform):

class MultiTransitionMovingPlatform(Platform):
_default_movable_class = MultiTransitionMovable


class Obstacle(Platform):
"""A platform class representing obstacles in the environment that may
block the line of sight to targets, preventing detection and measurement."""

shape_data: StateVectors = Property(
default=None,
doc="Coordinates defining the vertices of the obstacle relative"
"to its centroid without any orientation. Defaults to `None`")

simplices: Union[Sequence[int], np.ndarray] = Property(
default=None,
doc="A :class:`Sequence` or :class:`np.ndarray`, describing the connectivity "
"of vertices specified in :attr:`shape_data`. Should be constructed such "
"that element `i` is the index of a vertex that `i` is connected to. "
"For example, simplices for a four sided obstacle may be `(1, 2, 3, 0)` "
"for consecutively defined shape data. Default assumes that :attr:`shape_data` "
"is provided such that consecutive vertices are connected, such as the "
"example above.")

shape_mapping: Sequence[int] = Property(
default=(0, 1),
doc="A mapping for shape data dimensions to x y cartesian position. Default value is "
"(0,1)."
)

_default_movable_class = FixedMovable

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# If simplices not defined, calculate based on sequential vertices assumption
if self.simplices is None:
self.simplices = np.roll(np.linspace(0,
self.shape_data.shape[1]-1,
self.shape_data.shape[1]),
-1).astype(int)
# Initialise vertices
self._vertices = self._calculate_verts()

# Initialise relative_edges
self._relative_edges = self._calculate_relative_edges()

@property
def vertices(self):
"""Vertices are calculated by applying :attr:`position` and
:attr:`orientation` to :attr:`shape_data`. If :attr:`position`
or :attr:`orientation` changes, then vertices will reflect
these changes. If shape data specifies vertices that connect
to more than two other vertices, then the vertex with more
connections will be duplicated. This enables correct handling
of complex non-convex shapes."""
self._update_verts_and_relative_edges()
return self._vertices

@property
def relative_edges(self):
"""Calculates the difference between connected vertices
Cartesian coordinates. This is used by :meth:`is_visible` of
:class:`~.VisibilityInformed2DSensor` when calculating the
visibility of a state due to obstacles obstructing the line of
sight to the target."""
self._update_verts_and_relative_edges()
return self._relative_edges

@lru_cache(maxsize=None)
def _orientation_cache(self):
# Cache for orientation allows for vertices and relative edges to be
# be calculated when necessary. Maxsize set to unlimited as it
# is cleared before assigning a new value
return self.orientation

@lru_cache(maxsize=None)
def _position_cache(self):
# Cache for position allows for vertices and relative edges to be
# calculated only when necessary. Maxsize set to unlimited as it
# is cleared before assigning a new value
return self.position

def _update_verts_and_relative_edges(self):
# Checks to see if cached position and orientation matches the
# current property. If they match nothing is calculated. If they
# don't vertices and relative edges are recalculated.
if np.any(self._orientation_cache() != self.orientation) or \
np.any(self._position_cache() != self.position):

self._orientation_cache.cache_clear()
self._position_cache.cache_clear()
self._vertices = self._calculate_verts()
self._relative_edges = self._calculate_relative_edges()

def _calculate_verts(self) -> np.ndarray:
# Calculates the vertices based on the defined `shape_data`,
# `position` and `orientation`.
rot_mat = build_rotation_matrix(self.orientation)
rotated_shape = \
rot_mat[np.ix_(self.shape_mapping, self.shape_mapping)] @ \
self.shape_data[self.shape_mapping, :]
verts = rotated_shape + self.position
return verts[:, self.simplices]

def _calculate_relative_edges(self):
# Calculates the relative edge length in Cartesian space. Required
# for visibility estimator
return np.array(
[self.vertices[self.shape_mapping[0], :] -
self.vertices[self.shape_mapping[0],
np.roll(np.linspace(0,
len(self.simplices)-1,
len(self.simplices)), 1).astype(int)],
self.vertices[self.shape_mapping[1], :] -
self.vertices[self.shape_mapping[1],
np.roll(np.linspace(0,
len(self.simplices)-1,
len(self.simplices)), 1).astype(int)],
])

@classmethod
def from_obstacle(
cls,
obstacle: 'Obstacle',
*args: Any,
**kwargs: Any) -> 'Obstacle':

"""Return a new obstacle instance by providing new properties to an existing obstacle.
It is possible to overwrite any property of the original obstacle by
defining the required keyword arguments. Any arguments that are undefined
remain from the `obstacle` attribute. The utility of this method is to
easily create new obstacles from a single base obstacle, where each will
share the shape data of the original, but this is not the limit of its
functionality.

Parameters
----------
obstacle: Obstacle
:class:`~.Obstacle` to use existing properties from.
\\*args: Sequence
Arguments to pass to newly created obstacle which will replace those in `obstacle`
\\*\\*kwargs: Mapping
New property names and associate value for use in newly created obstacle, replacing
those on the ``obstacle`` parameter.
"""

args_property_names = {
name for n, name in enumerate(obstacle._properties) if n < len(args)}

ignore = ['movement_controller', 'id']

new_kwargs = {
name: getattr(obstacle, name)
for name in obstacle._properties.keys()
if name not in args_property_names and name not in kwargs and name not in ignore}

new_kwargs.update(kwargs)

if 'position_mapping' not in kwargs.keys():
new_kwargs.update({'position_mapping': getattr(obstacle, 'position_mapping')})

return cls(*args, **new_kwargs)
Loading