robometrics 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ """RoboMetrics: lightweight robotics metrics for Python."""
2
+
3
+ from robometrics.comfort import (
4
+ acceleration,
5
+ jerk,
6
+ jerk_cost,
7
+ max_acceleration,
8
+ max_deceleration,
9
+ smoothness_score,
10
+ )
11
+ from robometrics.evaluator import EvaluationInputError, Evaluator
12
+ from robometrics.io import TrajectoryIOError
13
+ from robometrics.physics import (
14
+ acceleration_limits_violated,
15
+ curvature_limits_violated,
16
+ dynamic_feasibility_score,
17
+ jerk_limits_violated,
18
+ speed_profile,
19
+ )
20
+ from robometrics.prediction import min_ade, min_fde, miss_rate, topk_trajectory_error
21
+ from robometrics.registry import MetricDefinition, MetricRegistry, UnknownMetricError, registry
22
+ from robometrics.results import EvaluationResult, MetricResult
23
+ from robometrics.safety import (
24
+ collision_rate,
25
+ lane_departure_rate,
26
+ min_distance_to_actors,
27
+ time_to_collision,
28
+ )
29
+ from robometrics.schemas import AgentState, Trajectory
30
+ from robometrics.trajectory import (
31
+ average_displacement_error,
32
+ curvature,
33
+ final_displacement_error,
34
+ hausdorff_distance,
35
+ lateral_error,
36
+ longitudinal_error,
37
+ path_length,
38
+ )
39
+
40
+ ade = average_displacement_error
41
+ fde = final_displacement_error
42
+
43
+ __all__ = [
44
+ "AgentState",
45
+ "EvaluationInputError",
46
+ "EvaluationResult",
47
+ "Evaluator",
48
+ "MetricDefinition",
49
+ "MetricRegistry",
50
+ "MetricResult",
51
+ "Trajectory",
52
+ "TrajectoryIOError",
53
+ "UnknownMetricError",
54
+ "acceleration",
55
+ "acceleration_limits_violated",
56
+ "ade",
57
+ "average_displacement_error",
58
+ "collision_rate",
59
+ "curvature",
60
+ "curvature_limits_violated",
61
+ "dynamic_feasibility_score",
62
+ "fde",
63
+ "final_displacement_error",
64
+ "hausdorff_distance",
65
+ "jerk",
66
+ "jerk_cost",
67
+ "jerk_limits_violated",
68
+ "lane_departure_rate",
69
+ "lateral_error",
70
+ "longitudinal_error",
71
+ "max_acceleration",
72
+ "max_deceleration",
73
+ "min_ade",
74
+ "min_distance_to_actors",
75
+ "min_fde",
76
+ "miss_rate",
77
+ "path_length",
78
+ "registry",
79
+ "smoothness_score",
80
+ "speed_profile",
81
+ "time_to_collision",
82
+ "topk_trajectory_error",
83
+ ]
84
+
85
+ __version__ = "0.1.0"
robometrics/comfort.py ADDED
@@ -0,0 +1,72 @@
1
+ """Planning and control smoothness metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ import numpy as np
8
+ from numpy.typing import ArrayLike
9
+
10
+ from robometrics.geometry import FloatArray, as_trajectory, validate_positive, vector_norms
11
+
12
+
13
+ def acceleration(traj: ArrayLike, dt: float) -> FloatArray:
14
+ """Return approximate acceleration vectors from a position trajectory."""
15
+ traj_arr = as_trajectory(traj, name="traj")
16
+ timestep = validate_positive(float(dt), name="dt")
17
+ if traj_arr.shape[0] < 3:
18
+ return np.zeros_like(traj_arr, dtype=np.float64)
19
+ velocity = _gradient(traj_arr, timestep)
20
+ return _gradient(velocity, timestep)
21
+
22
+
23
+ def jerk(traj: ArrayLike, dt: float) -> FloatArray:
24
+ """Return approximate jerk vectors from a position trajectory."""
25
+ accel = acceleration(traj, dt)
26
+ if accel.shape[0] < 3:
27
+ return np.zeros_like(accel, dtype=np.float64)
28
+ return _gradient(accel, validate_positive(float(dt), name="dt"))
29
+
30
+
31
+ def jerk_cost(traj: ArrayLike, dt: float) -> float:
32
+ """Return mean squared jerk magnitude."""
33
+ jerk_values = jerk(traj, dt)
34
+ return float(np.mean(np.square(vector_norms(jerk_values))))
35
+
36
+
37
+ def max_acceleration(traj: ArrayLike, dt: float) -> float:
38
+ """Return maximum acceleration magnitude."""
39
+ accel = acceleration(traj, dt)
40
+ return float(np.max(vector_norms(accel)))
41
+
42
+
43
+ def max_deceleration(traj: ArrayLike, dt: float) -> float:
44
+ """Return maximum longitudinal deceleration magnitude."""
45
+ traj_arr = as_trajectory(traj, name="traj")
46
+ timestep = validate_positive(float(dt), name="dt")
47
+ if traj_arr.shape[0] < 3:
48
+ return 0.0
49
+
50
+ velocity = _gradient(traj_arr, timestep)
51
+ accel = _gradient(velocity, timestep)
52
+ speeds = vector_norms(velocity)
53
+ moving = speeds > 1e-12
54
+ if not np.any(moving):
55
+ return 0.0
56
+
57
+ velocity_unit = np.zeros_like(velocity, dtype=np.float64)
58
+ velocity_unit[moving] = velocity[moving] / speeds[moving, None]
59
+ longitudinal_accel = np.sum(accel * velocity_unit, axis=1)
60
+ deceleration = np.maximum(-longitudinal_accel, 0.0)
61
+ return float(np.max(deceleration))
62
+
63
+
64
+ def smoothness_score(traj: ArrayLike, dt: float) -> float:
65
+ """Return a bounded smoothness score where 1.0 is smoother and 0.0 is worse."""
66
+ return float(1.0 / (1.0 + jerk_cost(traj, dt)))
67
+
68
+
69
+ def _gradient(values: FloatArray, dt: float) -> FloatArray:
70
+ edge_order: Literal[1, 2] = 2 if values.shape[0] > 2 else 1
71
+ gradient = np.gradient(values, dt, axis=0, edge_order=edge_order)
72
+ return np.asarray(gradient, dtype=np.float64)
@@ -0,0 +1,294 @@
1
+ """Lightweight local evaluator for named metric functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+ from robometrics.registry import MetricDefinition, MetricRegistry, registry
11
+ from robometrics.results import EvaluationResult, MetricResult
12
+
13
+
14
+ class EvaluationInputError(ValueError):
15
+ """Raised when an evaluation request cannot be constructed."""
16
+
17
+
18
+ class Evaluator:
19
+ """Evaluate registered RoboMetrics metrics against local Python inputs."""
20
+
21
+ def __init__(self, metric_registry: MetricRegistry | None = None) -> None:
22
+ self.registry = metric_registry or registry
23
+
24
+ def evaluate(
25
+ self,
26
+ *,
27
+ prediction: Any | None = None,
28
+ ground_truth: Any | None = None,
29
+ metrics: str | Sequence[str] | None = "all",
30
+ categories: str | Sequence[str] | None = None,
31
+ thresholds: Mapping[str, float] | None = None,
32
+ metric_kwargs: Mapping[str, Mapping[str, Any]] | None = None,
33
+ **inputs: Any,
34
+ ) -> EvaluationResult:
35
+ """Run selected metrics and return an EvaluationResult.
36
+
37
+ ``prediction`` and ``ground_truth`` are mapped onto the common argument
38
+ names used by built-in metric functions. Additional metric-specific
39
+ inputs, such as ``dt`` or ``actor_trajs``, can be supplied as keyword
40
+ arguments.
41
+ """
42
+ _validate_common_array(prediction, name="prediction", allowed_ndims=(2, 3))
43
+ _validate_common_array(ground_truth, name="ground_truth", allowed_ndims=(2,))
44
+ input_values = _build_inputs(
45
+ prediction=prediction,
46
+ ground_truth=ground_truth,
47
+ inputs=inputs,
48
+ )
49
+ if not input_values:
50
+ raise EvaluationInputError("at least one evaluation input is required")
51
+
52
+ selected, automatic_selection = self._select_metrics(metrics=metrics, categories=categories)
53
+ threshold_map = self._normalize_thresholds(thresholds)
54
+ metric_kwargs_map = self._normalize_metric_kwargs(metric_kwargs)
55
+
56
+ results: list[MetricResult] = []
57
+ skipped_metrics: list[str] = []
58
+ for metric in selected:
59
+ if not metric.is_compatible(input_values):
60
+ if automatic_selection:
61
+ skipped_metrics.append(metric.name)
62
+ continue
63
+ results.append(
64
+ _error_result(
65
+ metric,
66
+ "provided inputs are not compatible with this metric",
67
+ )
68
+ )
69
+ continue
70
+
71
+ try:
72
+ result = self._run_metric(metric, input_values, metric_kwargs_map)
73
+ except Exception as exc: # noqa: BLE001 - failures must be stored per metric.
74
+ result = _error_result(metric, str(exc), exc)
75
+
76
+ threshold = threshold_map.get(metric.name)
77
+ if threshold is not None and "error" not in result.metadata:
78
+ result.threshold = threshold
79
+ result.passed = bool(result.value <= threshold)
80
+ results.append(result)
81
+
82
+ if not results and selected:
83
+ raise EvaluationInputError("no compatible metrics found for the provided inputs")
84
+
85
+ return EvaluationResult(
86
+ results=results,
87
+ metadata={
88
+ "selected_metrics": [metric.name for metric in selected],
89
+ "skipped_metrics": skipped_metrics,
90
+ "categories": _category_list(categories),
91
+ },
92
+ )
93
+
94
+ def _select_metrics(
95
+ self,
96
+ *,
97
+ metrics: str | Sequence[str] | None,
98
+ categories: str | Sequence[str] | None,
99
+ ) -> tuple[list[MetricDefinition], bool]:
100
+ requested_categories = _category_list(categories)
101
+ if requested_categories:
102
+ known_categories = {category.lower() for category in self.registry.categories()}
103
+ unknown_categories = [
104
+ category
105
+ for category in requested_categories
106
+ if category.lower() not in known_categories
107
+ ]
108
+ if unknown_categories:
109
+ raise EvaluationInputError(
110
+ "unknown metric categories: " + ", ".join(unknown_categories)
111
+ )
112
+
113
+ automatic_selection = metrics is None or (
114
+ isinstance(metrics, str) and _normalize_name(metrics) == "all"
115
+ )
116
+
117
+ if automatic_selection:
118
+ selected = self.registry.list_metrics()
119
+ if requested_categories:
120
+ requested = {category.lower() for category in requested_categories}
121
+ selected = [metric for metric in selected if metric.category.lower() in requested]
122
+ return selected, True
123
+
124
+ requested_names = [metrics] if isinstance(metrics, str) else list(metrics or ())
125
+
126
+ selected = []
127
+ seen: set[str] = set()
128
+ for name in requested_names:
129
+ metric = self.registry.get(name)
130
+ if metric.name not in seen:
131
+ selected.append(metric)
132
+ seen.add(metric.name)
133
+ return selected, False
134
+
135
+ def _normalize_thresholds(
136
+ self,
137
+ thresholds: Mapping[str, float] | None,
138
+ ) -> dict[str, float]:
139
+ normalized: dict[str, float] = {}
140
+ for name, threshold in (thresholds or {}).items():
141
+ metric = self.registry.get(name)
142
+ value = float(threshold)
143
+ if not np.isfinite(value):
144
+ raise EvaluationInputError(f"threshold for {name} must be finite")
145
+ normalized[metric.name] = value
146
+ return normalized
147
+
148
+ def _normalize_metric_kwargs(
149
+ self,
150
+ metric_kwargs: Mapping[str, Mapping[str, Any]] | None,
151
+ ) -> dict[str, dict[str, Any]]:
152
+ normalized: dict[str, dict[str, Any]] = {}
153
+ for name, kwargs in (metric_kwargs or {}).items():
154
+ metric = self.registry.get(name)
155
+ normalized[metric.name] = dict(kwargs)
156
+ return normalized
157
+
158
+ def _run_metric(
159
+ self,
160
+ metric: MetricDefinition,
161
+ inputs: Mapping[str, Any],
162
+ metric_kwargs: Mapping[str, Mapping[str, Any]],
163
+ ) -> MetricResult:
164
+ kwargs: dict[str, Any] = {key: inputs[key] for key in metric.required_inputs}
165
+ kwargs.update(metric.default_kwargs)
166
+ for key in metric.default_kwargs:
167
+ if key in inputs:
168
+ kwargs[key] = inputs[key]
169
+ kwargs.update(metric_kwargs.get(metric.name, {}))
170
+
171
+ raw_value = metric.fn(**kwargs)
172
+ return _result_from_raw(metric, raw_value)
173
+
174
+
175
+ def _build_inputs(
176
+ *,
177
+ prediction: Any | None,
178
+ ground_truth: Any | None,
179
+ inputs: Mapping[str, Any],
180
+ ) -> dict[str, Any]:
181
+ values = {key: value for key, value in inputs.items() if value is not None}
182
+ if prediction is not None:
183
+ values["prediction"] = prediction
184
+ for alias in ("pred", "predictions", "traj", "trajectory", "ego_traj"):
185
+ values.setdefault(alias, prediction)
186
+ if ground_truth is not None:
187
+ values["ground_truth"] = ground_truth
188
+ for alias in ("gt", "ref", "reference"):
189
+ values.setdefault(alias, ground_truth)
190
+ return values
191
+
192
+
193
+ def _validate_common_array(
194
+ value: Any | None,
195
+ *,
196
+ name: str,
197
+ allowed_ndims: tuple[int, ...],
198
+ ) -> None:
199
+ if value is None:
200
+ return
201
+ try:
202
+ arr = np.asarray(value, dtype=np.float64)
203
+ except (TypeError, ValueError) as exc:
204
+ raise EvaluationInputError(f"{name} must be a numeric array-like value") from exc
205
+
206
+ if arr.ndim not in allowed_ndims:
207
+ allowed = " or ".join(str(ndim) for ndim in allowed_ndims)
208
+ raise EvaluationInputError(f"{name} must have {allowed} dimensions")
209
+ if arr.ndim == 2 and (arr.shape[0] == 0 or arr.shape[1] not in (2, 3)):
210
+ raise EvaluationInputError(f"{name} must be a non-empty Nx2 or Nx3 array")
211
+ if arr.ndim == 3 and (arr.shape[0] == 0 or arr.shape[1] == 0 or arr.shape[2] not in (2, 3)):
212
+ raise EvaluationInputError(f"{name} must be a non-empty KxTx2 or KxTx3 array")
213
+ if not np.all(np.isfinite(arr)):
214
+ raise EvaluationInputError(f"{name} must contain only finite values")
215
+
216
+
217
+ def _result_from_raw(metric: MetricDefinition, raw_value: Any) -> MetricResult:
218
+ if isinstance(raw_value, MetricResult):
219
+ metadata = dict(raw_value.metadata)
220
+ metadata.setdefault("category", metric.category)
221
+ metadata.setdefault("description", metric.description)
222
+ return MetricResult(
223
+ name=metric.name,
224
+ value=raw_value.value,
225
+ unit=raw_value.unit or metric.unit,
226
+ passed=raw_value.passed,
227
+ threshold=raw_value.threshold,
228
+ metadata=metadata,
229
+ )
230
+
231
+ value, metadata = _coerce_metric_value(raw_value)
232
+ metadata.setdefault("category", metric.category)
233
+ metadata.setdefault("description", metric.description)
234
+ return MetricResult(name=metric.name, value=value, unit=metric.unit, metadata=metadata)
235
+
236
+
237
+ def _coerce_metric_value(raw_value: Any) -> tuple[float, dict[str, Any]]:
238
+ arr = np.asarray(raw_value, dtype=np.float64)
239
+ if arr.ndim == 0:
240
+ return float(arr), {}
241
+ if arr.size == 0:
242
+ return float("nan"), {"reduction": "empty", "raw_value": []}
243
+
244
+ if arr.ndim >= 2 and arr.shape[-1] in (2, 3):
245
+ reduced = np.linalg.norm(arr, axis=-1)
246
+ reduction = "mean_norm"
247
+ else:
248
+ reduced = arr
249
+ reduction = "mean"
250
+
251
+ finite_values = reduced[np.isfinite(reduced)]
252
+ value = float(np.mean(finite_values)) if finite_values.size else float("nan")
253
+ return (
254
+ value,
255
+ {
256
+ "reduction": reduction,
257
+ "shape": list(arr.shape),
258
+ "value_count": int(arr.size),
259
+ "raw_value": arr.tolist(),
260
+ },
261
+ )
262
+
263
+
264
+ def _error_result(
265
+ metric: MetricDefinition,
266
+ message: str,
267
+ exc: Exception | None = None,
268
+ ) -> MetricResult:
269
+ metadata: dict[str, Any] = {
270
+ "category": metric.category,
271
+ "description": metric.description,
272
+ "error": message,
273
+ }
274
+ if exc is not None:
275
+ metadata["error_type"] = type(exc).__name__
276
+ return MetricResult(
277
+ name=metric.name,
278
+ value=float("nan"),
279
+ unit=metric.unit,
280
+ passed=False,
281
+ metadata=metadata,
282
+ )
283
+
284
+
285
+ def _category_list(categories: str | Sequence[str] | None) -> list[str]:
286
+ if categories is None:
287
+ return []
288
+ if isinstance(categories, str):
289
+ return [categories]
290
+ return list(categories)
291
+
292
+
293
+ def _normalize_name(name: str) -> str:
294
+ return name.strip().lower().replace("-", "_").replace(" ", "_")
@@ -0,0 +1,164 @@
1
+ """Geometry helpers shared by RoboMetrics modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import TypeAlias
7
+
8
+ import numpy as np
9
+ from numpy.typing import ArrayLike, NDArray
10
+
11
+ FloatArray: TypeAlias = NDArray[np.float64]
12
+
13
+
14
+ def as_trajectory(data: ArrayLike, *, name: str = "trajectory") -> FloatArray:
15
+ """Return a finite non-empty Nx2 or Nx3 trajectory array."""
16
+ arr = np.asarray(data, dtype=np.float64)
17
+ if arr.ndim != 2 or arr.shape[1] not in (2, 3):
18
+ raise ValueError(f"{name} must be a Nx2 or Nx3 array")
19
+ if arr.shape[0] == 0:
20
+ raise ValueError(f"{name} must contain at least one point")
21
+ if not np.all(np.isfinite(arr)):
22
+ raise ValueError(f"{name} must contain only finite values")
23
+ return arr
24
+
25
+
26
+ def as_prediction_set(data: ArrayLike, *, name: str = "predictions") -> FloatArray:
27
+ """Return a finite non-empty KxTx2 or KxTx3 prediction array."""
28
+ arr = np.asarray(data, dtype=np.float64)
29
+ if arr.ndim != 3 or arr.shape[2] not in (2, 3):
30
+ raise ValueError(f"{name} must be a KxTx2 or KxTx3 array")
31
+ if arr.shape[0] == 0 or arr.shape[1] == 0:
32
+ raise ValueError(f"{name} must contain at least one trajectory and one timestep")
33
+ if not np.all(np.isfinite(arr)):
34
+ raise ValueError(f"{name} must contain only finite values")
35
+ return arr
36
+
37
+
38
+ def require_same_shape(
39
+ left: FloatArray,
40
+ right: FloatArray,
41
+ left_name: str,
42
+ right_name: str,
43
+ ) -> None:
44
+ """Validate that two arrays have identical shape."""
45
+ if left.shape != right.shape:
46
+ raise ValueError(f"{left_name} and {right_name} must have the same shape")
47
+
48
+
49
+ def require_same_time_and_dim(
50
+ predictions: FloatArray,
51
+ ground_truth: FloatArray,
52
+ predictions_name: str = "predictions",
53
+ ground_truth_name: str = "gt",
54
+ ) -> None:
55
+ """Validate that KxTxD predictions match TxD ground truth."""
56
+ if predictions.shape[1:] != ground_truth.shape:
57
+ raise ValueError(
58
+ f"{predictions_name} timesteps/dimensions must match {ground_truth_name}"
59
+ )
60
+
61
+
62
+ def xy(data: FloatArray) -> FloatArray:
63
+ """Return XY columns from an Nx2/Nx3 trajectory-like array."""
64
+ return data[:, :2]
65
+
66
+
67
+ def vector_norms(data: FloatArray) -> FloatArray:
68
+ """Return row-wise Euclidean norms."""
69
+ return np.asarray(np.linalg.norm(data, axis=1), dtype=np.float64)
70
+
71
+
72
+ def pointwise_distances(left: FloatArray, right: FloatArray) -> FloatArray:
73
+ """Return pointwise Euclidean distances for equally shaped trajectories."""
74
+ require_same_shape(left, right, "left", "right")
75
+ return vector_norms(left - right)
76
+
77
+
78
+ def validate_positive(value: float, *, name: str) -> float:
79
+ """Return value if positive, otherwise raise ValueError."""
80
+ if not np.isfinite(value) or value <= 0.0:
81
+ raise ValueError(f"{name} must be a positive finite value")
82
+ return float(value)
83
+
84
+
85
+ def validate_nonnegative(value: float, *, name: str) -> float:
86
+ """Return value if non-negative, otherwise raise ValueError."""
87
+ if not np.isfinite(value) or value < 0.0:
88
+ raise ValueError(f"{name} must be a non-negative finite value")
89
+ return float(value)
90
+
91
+
92
+ def as_actor_trajectories(actor_trajs: object) -> list[FloatArray]:
93
+ """Normalize actor trajectories to a list of finite Nx2/Nx3 arrays."""
94
+ try:
95
+ arr = np.asarray(actor_trajs, dtype=np.float64)
96
+ except (TypeError, ValueError):
97
+ if not isinstance(actor_trajs, Iterable):
98
+ raise ValueError("actor_trajs must be an actor trajectory or iterable") from None
99
+ return [
100
+ as_trajectory(actor, name=f"actor_trajs[{index}]")
101
+ for index, actor in enumerate(actor_trajs)
102
+ ]
103
+
104
+ if arr.size == 0:
105
+ return []
106
+ if arr.ndim == 2:
107
+ return [as_trajectory(arr, name="actor_trajs")]
108
+ if arr.ndim == 3:
109
+ return [
110
+ as_trajectory(arr[index], name=f"actor_trajs[{index}]")
111
+ for index in range(arr.shape[0])
112
+ ]
113
+ raise ValueError("actor_trajs must be Nx2/Nx3, MxNx2/MxNx3, or an iterable of trajectories")
114
+
115
+
116
+ def point_in_polygon(point: FloatArray, polygon: FloatArray, *, tolerance: float = 1e-9) -> bool:
117
+ """Return True when a 2D point lies inside or on the boundary of a polygon."""
118
+ px = float(point[0])
119
+ py = float(point[1])
120
+ poly = xy(polygon)
121
+ if poly.shape[0] < 3:
122
+ raise ValueError("lane_boundary must contain at least three polygon vertices")
123
+
124
+ inside = False
125
+ count = poly.shape[0]
126
+ for index in range(count):
127
+ start = poly[index]
128
+ end = poly[(index + 1) % count]
129
+ if _point_on_segment(np.array([px, py], dtype=np.float64), start, end, tolerance=tolerance):
130
+ return True
131
+
132
+ y_crosses = (start[1] > py) != (end[1] > py)
133
+ if y_crosses:
134
+ x_intersection = (end[0] - start[0]) * (py - start[1]) / (end[1] - start[1]) + start[0]
135
+ if px < x_intersection:
136
+ inside = not inside
137
+ return inside
138
+
139
+
140
+ def points_in_polygon(points: FloatArray, polygon: FloatArray) -> NDArray[np.bool_]:
141
+ """Return an inside-mask for points against a polygon."""
142
+ return np.asarray([point_in_polygon(point, polygon) for point in xy(points)], dtype=np.bool_)
143
+
144
+
145
+ def _point_on_segment(
146
+ point: FloatArray,
147
+ start: FloatArray,
148
+ end: FloatArray,
149
+ *,
150
+ tolerance: float,
151
+ ) -> bool:
152
+ segment = end - start
153
+ point_delta = point - start
154
+ squared_length = float(np.dot(segment, segment))
155
+ if squared_length <= tolerance * tolerance:
156
+ return bool(np.linalg.norm(point_delta) <= tolerance)
157
+
158
+ cross = abs(float(segment[0] * point_delta[1] - segment[1] * point_delta[0]))
159
+ if cross > tolerance:
160
+ return False
161
+ dot = float(np.dot(point_delta, segment))
162
+ if dot < -tolerance:
163
+ return False
164
+ return dot <= squared_length + tolerance