mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-02-19 11:13:55 +08:00
version: lp-dp v0.9.5 for EON/C2 date: 2023-09-13T13:36:48 commit: 1d59b81c40b93f37d4a341c5d35b1c3cd293543d
394 lines
16 KiB
Python
394 lines
16 KiB
Python
import numpy as np
|
|
from enum import Enum
|
|
from openpilot.selfdrive.mapd.lib.geo import DIRECTION, R, vectors
|
|
|
|
# from scipy.interpolate import splev, splprep
|
|
from opspline import splev, splprep # pylint: disable=E0401
|
|
|
|
|
|
_TURN_CURVATURE_THRESHOLD = 0.002 # 1/mts. A curvature over this value will generate a speed limit section.
|
|
_MAX_LAT_ACC = 2.3 # Maximum lateral acceleration in turns.
|
|
_SPLINE_EVAL_STEP = 5 # mts for spline evaluation for curvature calculation
|
|
_MIN_SPEED_SECTION_LENGTH = 100. # mts. Sections below this value will not be split in smaller sections.
|
|
_MAX_CURV_DEVIATION_FOR_SPLIT = 2. # Split a speed section if the max curvature deviates from mean by this factor.
|
|
_MAX_CURV_SPLIT_ARC_ANGLE = 90. # degrees. Arc section to split into new speed section around max curvature.
|
|
_MIN_NODE_DISTANCE = 50. # mts. Minimum distance between nodes for spline evaluation. Data is enhanced if not met.
|
|
_ADDED_NODES_DIST = 15. # mts. Distance between added nodes when data is enhanced for spline evaluation.
|
|
_DIVERTION_SEARCH_RANGE = [-200., 50.] # mt. Range of distance to current location for divertion search.
|
|
|
|
|
|
def nodes_raw_data_array_for_wr(wr, drop_last=False):
|
|
"""Provides an array of raw node data (id, lat, lon, speed_limit) for all nodes in way relation
|
|
"""
|
|
sl = wr.speed_limit
|
|
data = np.array([(n.id, n.lat, n.lon, sl) for n in wr.way.nodes], dtype=float)
|
|
|
|
# reverse the order if way direction is backwards
|
|
if wr.direction == DIRECTION.BACKWARD:
|
|
data = np.flip(data, axis=0)
|
|
|
|
# drop last if requested
|
|
return data[:-1] if drop_last else data
|
|
|
|
|
|
def node_calculations(points):
|
|
"""Provides node calculations based on an array of (lat, lon) points in radians.
|
|
points is a (N x 1) array where N >= 3
|
|
"""
|
|
if len(points) < 3:
|
|
raise(IndexError)
|
|
|
|
# Get the vector representation of node points in cartesian plane.
|
|
# (N-1, 2) array. Not including (0., 0.)
|
|
v = vectors(points) * R
|
|
|
|
# Calculate the vector magnitudes (or distance)
|
|
# (N-1, 1) array. No distance for v[-1]
|
|
d = np.linalg.norm(v, axis=1)
|
|
|
|
# Calculate the bearing (from true north clockwise) for every node.
|
|
# (N-1, 1) array. No bearing for v[-1]
|
|
b = np.arctan2(v[:, 0], v[:, 1])
|
|
|
|
# Add origin to vector space. (i.e first node in list)
|
|
v = np.concatenate(([[0., 0.]], v))
|
|
|
|
# Provide distance to previous node and distance to next node
|
|
dp = np.concatenate(([0.], d))
|
|
dn = np.concatenate((d, [0.]))
|
|
|
|
# Provide cumulative distance on route
|
|
dr = np.cumsum(dp, axis=0)
|
|
|
|
# Bearing of last node should keep bearing from previous.
|
|
b = np.concatenate((b, [b[-1]]))
|
|
|
|
return v, dp, dn, dr, b
|
|
|
|
|
|
def spline_curvature_calculations(vect, dist_prev):
|
|
"""Provides an array of curvatures and its distances by applying a spline interpolation
|
|
to the path described by the nodes data.
|
|
"""
|
|
# We need to artificially enhance the data before applying spline interpolation to avoid getting
|
|
# inexistent curvature values close to irregularities on the road when the resolution of nodes data
|
|
# approaching the irregularity is low.
|
|
|
|
# - Find indexes where dist_prev is greater than threshold
|
|
too_far_idxs = np.nonzero(dist_prev >= _MIN_NODE_DISTANCE)[0]
|
|
|
|
# - Traversing in reverse order, enhance data by adding points at the found indexes.
|
|
for idx in too_far_idxs[::-1]:
|
|
dp = dist_prev[idx] # distance of vector that needs to be replaced by higher resolution vectors.
|
|
n = int(np.ceil(dp / _ADDED_NODES_DIST)) # number of vectors that need to be added.
|
|
new_v = vect[idx, :] / n # new relative vector to insert.
|
|
vect = np.delete(vect, idx, axis=0) # remove the relative vector to be replaced by the insertion of new vectors.
|
|
vect = np.insert(vect, [idx] * n, [new_v] * n, axis=0) # insert n new relative vectors
|
|
|
|
# Data is now enhanced, we can proceed with curvature evaluation.
|
|
# - Create cumulative arrays for distance traveled and vector (x, y)
|
|
ds = np.cumsum(dist_prev, axis=0)
|
|
vs = np.cumsum(vect, axis=0)
|
|
|
|
# - spline interpolation
|
|
tck, u = splprep([vs[:, 0], vs[:, 1]]) # pylint: disable=unbalanced-tuple-unpacking
|
|
|
|
# - evaluate every _SPLINE_EVAL_STEP mts.
|
|
n = max(int(ds[-1] / _SPLINE_EVAL_STEP), len(u))
|
|
unew = np.arange(0, n + 1) / n
|
|
|
|
# - get derivatives
|
|
d1 = splev(unew, tck, der=1)
|
|
d2 = splev(unew, tck, der=2)
|
|
|
|
# - calculate curvatures
|
|
num = d1[0] * d2[1] - d1[1] * d2[0]
|
|
den = (d1[0]**2 + d1[1]**2)**(1.5)
|
|
curv = num / den
|
|
curv_ds = unew * ds[-1]
|
|
|
|
return curv, curv_ds
|
|
|
|
|
|
def speed_section(curv_sec):
|
|
"""Map curvature section data into turn speed sections data.
|
|
Returns: [section start distance, section end distance, speed limit based on max curvature, sing of curvature]
|
|
"""
|
|
max_curv_idx = np.argmax(curv_sec[:, 0])
|
|
start = np.amin(curv_sec[:, 2])
|
|
end = np.amax(curv_sec[:, 2])
|
|
|
|
return np.array([start, end, np.sqrt(_MAX_LAT_ACC / curv_sec[max_curv_idx, 0]), curv_sec[max_curv_idx, 1]])
|
|
|
|
|
|
def split_speed_section_by_sign(curv_sec):
|
|
"""Will split the given curvature section in subsections if there is a change of sign on the curvature value
|
|
in the section.
|
|
"""
|
|
# Find the indexes where the curvatures change signs (if any).
|
|
c_idx = np.nonzero(np.diff(curv_sec[:, 1]))[0] + 1
|
|
|
|
# Split section base on change of sign.
|
|
return np.split(curv_sec, c_idx)
|
|
|
|
|
|
def split_speed_section_by_curv_degree(curv_sec):
|
|
"""Will split the given curvature section in subsections as to isolate peaks of turn with substantially
|
|
higher curvature values. This will aid on preventing having very long turn sections with low speed limit
|
|
that is only really necessary for a small region of the section.
|
|
"""
|
|
# Only consider spliting a section if long enough.
|
|
length = curv_sec[-1, 2] - curv_sec[0, 2]
|
|
if length <= _MIN_SPEED_SECTION_LENGTH:
|
|
return [curv_sec]
|
|
|
|
# Only split if max curvature deviates substantially from mean curvature.
|
|
max_curv_idx = np.argmax(curv_sec[:, 0])
|
|
max_curv = curv_sec[max_curv_idx, 0]
|
|
mean_curv = np.mean(curv_sec[:, 0])
|
|
if max_curv / mean_curv <= _MAX_CURV_DEVIATION_FOR_SPLIT:
|
|
return [curv_sec]
|
|
|
|
# Calcualate where to split as to isolate a curve section around the max curvature peak.
|
|
arc_side = (np.radians(_MAX_CURV_SPLIT_ARC_ANGLE) / max_curv) / 2.
|
|
arc_side_idx_lenght = int(np.ceil(arc_side / _SPLINE_EVAL_STEP))
|
|
split_idxs = [max_curv_idx - arc_side_idx_lenght, max_curv_idx + arc_side_idx_lenght]
|
|
split_idxs = list(filter(lambda idx: idx > 0 and idx < len(curv_sec) - 1, split_idxs))
|
|
|
|
# If the arc section to split extendes outside the section, then no need to split.
|
|
if len(split_idxs) == 0:
|
|
return [curv_sec]
|
|
|
|
# Create the splits and split the resulting sections recursevly.
|
|
splits = [split_speed_section_by_curv_degree(cs) for cs in np.split(curv_sec, split_idxs)]
|
|
|
|
# Flatten the results and return the new list of curvature sections.
|
|
curv_secs = [cs for split in splits for cs in split]
|
|
return curv_secs
|
|
|
|
|
|
def speed_limits_for_curvatures_data(curv, dist):
|
|
"""Provides the calculations for the speed limits from the curvatures array and distances,
|
|
by providing distances to curvature sections and correspoinding speed limit values as well as
|
|
curvature direction/sign.
|
|
"""
|
|
# Prepare a data array for processing with absolute curvature values, curvature sign and distances.
|
|
curv_abs = np.abs(curv)
|
|
data = np.column_stack((curv_abs, np.sign(curv), dist))
|
|
|
|
# Find where curvatures overshoot turn curvature threshold and define as section
|
|
is_section = curv_abs >= _TURN_CURVATURE_THRESHOLD
|
|
|
|
# Find the indexes where the sections start and end. i.e. change indexes.
|
|
c_idx = np.nonzero(np.diff(is_section))[0] + 1
|
|
|
|
# Create independent arrays for each split section base on change indexes.
|
|
splits = np.array(np.split(data, c_idx), dtype=object)
|
|
|
|
# Filter the splits to keep only the curvature section arrays by getting the odd or even split arrays depending
|
|
# on whether the first split is a curvature split or not.
|
|
curv_sec_idxs = np.arange(0 if is_section[0] else 1, len(splits), 2, dtype=int)
|
|
curv_secs = splits[curv_sec_idxs]
|
|
|
|
# Further split the curv sections by sign change
|
|
sub_secs = [split_speed_section_by_sign(cs) for cs in curv_secs]
|
|
curv_secs = [cs for sub_sec in sub_secs for cs in sub_sec]
|
|
|
|
# Further split the curv sections by degree of curvature
|
|
sub_secs = [split_speed_section_by_curv_degree(cs) for cs in curv_secs]
|
|
curv_secs = [cs for sub_sec in sub_secs for cs in sub_sec]
|
|
|
|
# Return an array where each row represents a turn speed limit section.
|
|
# [start, end, speed_limit, curvature_sign]
|
|
return np.array([speed_section(cs) for cs in curv_secs])
|
|
|
|
def is_wr_a_valid_divertion_from_node(wr, node_id, wr_ids):
|
|
"""
|
|
Evaluates if the way relation `wr` is a valid divertion from node with id `node_id`.
|
|
A valid divertion is a way relation with an edge node with the given `node_id` that is not already included
|
|
in the list of way relations in the route (`wr_ids`) and that can be travaled in the direction as if starting
|
|
from node with id `node_id`
|
|
"""
|
|
if wr.id in wr_ids:
|
|
return False
|
|
wr.update_direction_from_starting_node(node_id)
|
|
return not wr.is_prohibited
|
|
|
|
|
|
class SpeedLimitSection():
|
|
"""And object representing a speed limited road section ahead.
|
|
provides the start and end distance and the speed limit value
|
|
"""
|
|
def __init__(self, start, end, value):
|
|
self.start = start
|
|
self.end = end
|
|
self.value = value
|
|
|
|
def __repr__(self):
|
|
return f'from: {self.start}, to: {self.end}, limit: {self.value}'
|
|
|
|
|
|
class TurnSpeedLimitSection(SpeedLimitSection):
|
|
def __init__(self, start, end, value, sign):
|
|
super().__init__(start, end, value)
|
|
self.curv_sign = sign
|
|
|
|
def __repr__(self):
|
|
return f'{super().__repr__()}, sign: {self.curv_sign}'
|
|
|
|
|
|
class NodeDataIdx(Enum):
|
|
"""Column index for data elements on NodesData underlying data store.
|
|
"""
|
|
node_id = 0
|
|
lat = 1
|
|
lon = 2
|
|
speed_limit = 3
|
|
x = 4 # x value of cartesian vector representing the section between last node and this node.
|
|
y = 5 # y value of cartesian vector representing the section between last node and this node.
|
|
dist_prev = 6 # distance to previous node.
|
|
dist_next = 7 # distance to next node
|
|
dist_route = 8 # cumulative distance on route
|
|
bearing = 9 # bearing of the vector departing from this node.
|
|
|
|
|
|
class NodesData:
|
|
"""Container for the list of node data from a ordered list of way relations to be used in a Route
|
|
"""
|
|
def __init__(self, way_relations, wr_index):
|
|
self._nodes_data = np.array([])
|
|
self._divertions = [[]]
|
|
self._curvature_speed_sections_data = np.array([])
|
|
|
|
way_count = len(way_relations)
|
|
if way_count == 0:
|
|
return
|
|
|
|
# We want all the nodes from the last way section
|
|
nodes_data = nodes_raw_data_array_for_wr(way_relations[-1])
|
|
|
|
# For the ways before the last in the route we want all the nodes but the last, as that one is the first on
|
|
# the next section. Collect them, append last way node data and concatenate the numpy arrays.
|
|
if way_count > 1:
|
|
wrs_data = tuple([nodes_raw_data_array_for_wr(wr, drop_last=True) for wr in way_relations[:-1]])
|
|
wrs_data += (nodes_data,)
|
|
nodes_data = np.concatenate(wrs_data)
|
|
|
|
# Get a subarray with lat, lon to compute the remaining node values.
|
|
lat_lon_array = nodes_data[:, [1, 2]]
|
|
points = np.radians(lat_lon_array)
|
|
# Ensure we have more than 3 points, if not calculations are not possible.
|
|
if len(points) <= 3:
|
|
return
|
|
vect, dist_prev, dist_next, dist_route, bearing = node_calculations(points)
|
|
|
|
# append calculations to nodes_data
|
|
# nodes_data structure: [id, lat, lon, speed_limit, x, y, dist_prev, dist_next, dist_route, bearing]
|
|
self._nodes_data = np.column_stack((nodes_data, vect, dist_prev, dist_next, dist_route, bearing))
|
|
|
|
# Build route divertion options data from the wr_index.
|
|
wr_ids = [wr.id for wr in way_relations]
|
|
self._divertions = [[wr for wr in wr_index.way_relations_with_edge_node_id(node_id)
|
|
if is_wr_a_valid_divertion_from_node(wr, node_id, wr_ids)]
|
|
for node_id in nodes_data[:, 0]]
|
|
|
|
# Store calculcations for curvature sections speed limits. We need more than 3 points to be able to process.
|
|
# _curvature_speed_sections_data structure: [dist_start, dist_stop, speed_limits, curv_sign]
|
|
if len(vect) > 3:
|
|
curv, curv_ds = spline_curvature_calculations(vect, dist_prev)
|
|
self._curvature_speed_sections_data = speed_limits_for_curvatures_data(curv, curv_ds)
|
|
|
|
@property
|
|
def count(self):
|
|
return len(self._nodes_data)
|
|
|
|
def get(self, node_data_idx):
|
|
"""Returns the array containing all the elements of a specific NodeDataIdx type.
|
|
"""
|
|
if len(self._nodes_data) == 0 or node_data_idx.value >= self._nodes_data.shape[1]:
|
|
return np.array([])
|
|
|
|
return self._nodes_data[:, node_data_idx.value]
|
|
|
|
def speed_limits_ahead(self, ahead_idx, distance_to_node_ahead):
|
|
"""Returns and array of SpeedLimitSection objects for the actual route ahead of current location
|
|
"""
|
|
if len(self._nodes_data) == 0 or ahead_idx is None:
|
|
return []
|
|
|
|
# Find the cumulative distances where speed limit changes. Build Speed limit sections for those.
|
|
dist = np.concatenate(([distance_to_node_ahead], self.get(NodeDataIdx.dist_next)[ahead_idx:]))
|
|
dist = np.cumsum(dist, axis=0)
|
|
sl = self.get(NodeDataIdx.speed_limit)[ahead_idx - 1:]
|
|
sl_next = np.concatenate((sl[1:], [0.]))
|
|
|
|
# Create a boolean mask where speed limit changes and filter values
|
|
sl_change = sl != sl_next
|
|
distances = dist[sl_change]
|
|
speed_limits = sl[sl_change]
|
|
|
|
# Create speed limits sections combining all continious nodes that have same speed limit value.
|
|
start = 0.
|
|
limits_ahead = []
|
|
for idx, end in enumerate(distances):
|
|
limits_ahead.append(SpeedLimitSection(start, end, speed_limits[idx]))
|
|
start = end
|
|
|
|
return limits_ahead
|
|
|
|
def distance_to_end(self, ahead_idx, distance_to_node_ahead):
|
|
if len(self._nodes_data) == 0 or ahead_idx is None:
|
|
return None
|
|
|
|
return np.sum(np.concatenate(([distance_to_node_ahead], self.get(NodeDataIdx.dist_next)[ahead_idx:])))
|
|
|
|
def curvatures_speed_limit_sections_ahead(self, ahead_idx, distance_to_node_ahead):
|
|
"""Returns and array of TurnSpeedLimitSection objects for the actual route ahead of current location for
|
|
speed limit sections due to curvatures in the road.
|
|
"""
|
|
if len(self._curvature_speed_sections_data) == 0 or ahead_idx is None:
|
|
return []
|
|
|
|
# Find the current distance traveled so far on the route.
|
|
dist_curr = self.get(NodeDataIdx.dist_route)[ahead_idx] - distance_to_node_ahead
|
|
|
|
# Filter the sections to get only those where the stop distance is ahead of current.
|
|
sec_filter = self._curvature_speed_sections_data[:, 1] > dist_curr
|
|
data = self._curvature_speed_sections_data[sec_filter]
|
|
|
|
# Offset distances to current distance.
|
|
data[:, [0, 1]] -= dist_curr
|
|
|
|
# Create speed limits sections
|
|
limits_ahead = [TurnSpeedLimitSection(max(0., d[0]), d[1], d[2], d[3]) for d in data]
|
|
|
|
return limits_ahead
|
|
|
|
def possible_divertions(self, ahead_idx, distance_to_node_ahead):
|
|
""" Returns and array with the way relations the route could possible divert to by finding
|
|
the alternative way divertions on the nodes in the vicinity of the current location.
|
|
"""
|
|
if len(self._nodes_data) == 0 or ahead_idx is None:
|
|
return []
|
|
|
|
dist_route = self.get(NodeDataIdx.dist_route)
|
|
rel_dist = dist_route - dist_route[ahead_idx] + distance_to_node_ahead
|
|
valid_idxs = np.nonzero(np.logical_and(rel_dist >= _DIVERTION_SEARCH_RANGE[0],
|
|
rel_dist <= _DIVERTION_SEARCH_RANGE[1]))[0]
|
|
valid_divertions = [self._divertions[i] for i in valid_idxs]
|
|
|
|
return [wr for wrs in valid_divertions for wr in wrs] # flatten.
|
|
|
|
def distance_to_node(self, node_id, ahead_idx, distance_to_node_ahead):
|
|
"""
|
|
Provides the distance to a specific node in the route identified by `node_id` in reference to the node ahead
|
|
(`ahead_idx`) and the distance from current location to the node ahead (`distance_to_node_ahead`).
|
|
"""
|
|
node_ids = self.get(NodeDataIdx.node_id)
|
|
node_idxs = np.nonzero(node_ids == node_id)[0]
|
|
if len(self._nodes_data) == 0 or ahead_idx is None or len(node_idxs) == 0:
|
|
return None
|
|
|
|
return self.get(NodeDataIdx.dist_route)[node_idxs[0]] - self.get(NodeDataIdx.dist_route)[ahead_idx] + \
|
|
distance_to_node_ahead
|