Skip to content

Commit 3d28878

Browse files
committed
feat(planning_data_analyzer): add open-loop extended comfort metric
1 parent 38fc640 commit 3d28878

10 files changed

Lines changed: 441 additions & 2 deletions

planning/autoware_planning_data_analyzer/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ament_auto_add_library(${PROJECT_NAME} SHARED
1414
src/metrics/history_comfort.cpp
1515
src/metrics/lane_keeping.cpp
1616
src/metrics/drivable_area_compliance.cpp
17+
src/metrics/extended_comfort.cpp
1718
src/metrics/trajectory_metrics.cpp
1819
src/utils/bag_utils.cpp
1920
src/utils/json_utils.cpp
@@ -34,6 +35,7 @@ if(BUILD_TESTING)
3435
test/metrics/test_deviation_metrics.cpp
3536
test/metrics/test_lane_keeping.cpp
3637
test/metrics/test_drivable_area_compliance.cpp
38+
test/metrics/test_extended_comfort.cpp
3739
)
3840
target_link_libraries(test_metrics ${PROJECT_NAME})
3941

planning/autoware_planning_data_analyzer/config/planning_data_analyzer.param.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@
4949
# ADE/FDE are reported at these horizons plus "full" (entire trajectory).
5050
# Trajectories shorter than a horizon are excluded from that horizon with 0.1s tolerance.
5151
evaluation_horizons: [1.0, 2.0, 4.0, 8.0]
52+
extended_comfort:
53+
max_acceleration_rms: 0.7 # Maximum RMS acceleration discrepancy between consecutive plans [m/s^2]
54+
max_jerk_rms: 0.5 # Maximum RMS jerk discrepancy between consecutive plans [m/s^3]
55+
max_yaw_rate_rms: 0.1 # Maximum RMS yaw-rate discrepancy between consecutive plans [rad/s]
56+
max_yaw_acceleration_rms: 0.1 # Maximum RMS yaw-acceleration discrepancy between consecutive plans [rad/s^2]
57+
finite_difference_epsilon: 0.001 # Minimum delta time used for EC finite differences [s]
5258
lane_keep:
5359
max_lateral_deviation: 0.5 # Maximum absolute lateral deviation allowed for LK [m]
5460
max_continuous_violation_time: 2.0 # Continuous over-threshold duration allowed for LK [s]

planning/autoware_planning_data_analyzer/src/autoware_planning_data_analyzer_node.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ AutowarePlanningDataAnalyzerNode::AutowarePlanningDataAnalyzerNode(
134134
get_or_declare_parameter<double>(*this, "open_loop.hc.max_yaw_rate");
135135
history_comfort_params_.max_yaw_acceleration =
136136
get_or_declare_parameter<double>(*this, "open_loop.hc.max_yaw_acceleration");
137+
extended_comfort_parameters_.max_acceleration_rms =
138+
get_or_declare_parameter<double>(*this, "open_loop.extended_comfort.max_acceleration_rms");
139+
extended_comfort_parameters_.max_jerk_rms =
140+
get_or_declare_parameter<double>(*this, "open_loop.extended_comfort.max_jerk_rms");
141+
extended_comfort_parameters_.max_yaw_rate_rms =
142+
get_or_declare_parameter<double>(*this, "open_loop.extended_comfort.max_yaw_rate_rms");
143+
extended_comfort_parameters_.max_yaw_acceleration_rms =
144+
get_or_declare_parameter<double>(*this, "open_loop.extended_comfort.max_yaw_acceleration_rms");
145+
extended_comfort_parameters_.finite_difference_epsilon =
146+
get_or_declare_parameter<double>(*this, "open_loop.extended_comfort.finite_difference_epsilon");
137147
lane_keeping_params_.max_lateral_deviation =
138148
get_or_declare_parameter<double>(*this, "open_loop.lane_keep.max_lateral_deviation");
139149
lane_keeping_params_.max_continuous_violation_time =
@@ -487,6 +497,7 @@ void AutowarePlanningDataAnalyzerNode::run_evaluation()
487497
evaluator.set_json_output_dir(output_dir_path.string());
488498
evaluator.set_metric_variant(open_loop_metric_variant);
489499
evaluator.set_evaluation_horizons(evaluation_horizons);
500+
evaluator.set_extended_comfort_parameters(extended_comfort_parameters_);
490501
auto times =
491502
evaluator.run_evaluation_from_bag(bag_path_, evaluation_bag_writer_.get(), topic_names);
492503
start_time = times.first;

planning/autoware_planning_data_analyzer/src/autoware_planning_data_analyzer_node.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#define AUTOWARE_PLANNING_DATA_ANALYZER_NODE_HPP_
1717

1818
#include "bag_handler.hpp"
19+
#include "metrics/extended_comfort.hpp"
1920
#include "metrics/trajectory_metrics.hpp"
2021
#include "rosbag2_cpp/reader.hpp"
2122
#include "rosbag2_cpp/writer.hpp"
@@ -86,6 +87,7 @@ class AutowarePlanningDataAnalyzerNode : public rclcpp::Node
8687
std::string gt_trajectory_topic_name_;
8788
double gt_sync_tolerance_ms_ = 200.0;
8889
metrics::HistoryComfortParameters history_comfort_params_;
90+
metrics::ExtendedComfortParameters extended_comfort_parameters_;
8991
metrics::LaneKeepingParameters lane_keeping_params_;
9092
autoware::vehicle_info_utils::VehicleInfo vehicle_info_;
9193
std::string objects_topic_name_;
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright 2026 TIER IV, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "extended_comfort.hpp"
16+
17+
#include <autoware_utils_math/normalization.hpp>
18+
19+
#include <tf2/utils.h>
20+
21+
#include <algorithm>
22+
#include <cmath>
23+
#include <vector>
24+
25+
namespace autoware::planning_data_analyzer::metrics
26+
{
27+
28+
namespace
29+
{
30+
31+
struct DynamicSignals
32+
{
33+
std::vector<double> accelerations;
34+
std::vector<double> jerks;
35+
std::vector<double> yaw_rates;
36+
std::vector<double> yaw_accelerations;
37+
};
38+
39+
double get_time_seconds(const builtin_interfaces::msg::Duration & time_from_start)
40+
{
41+
return static_cast<double>(time_from_start.sec) +
42+
static_cast<double>(time_from_start.nanosec) * 1e-9;
43+
}
44+
45+
double compute_rms_difference(const std::vector<double> & lhs, const std::vector<double> & rhs)
46+
{
47+
const auto count = std::min(lhs.size(), rhs.size());
48+
if (count == 0U) {
49+
return 0.0;
50+
}
51+
52+
double sum_squared_error = 0.0;
53+
for (std::size_t i = 0; i < count; ++i) {
54+
const double error = lhs.at(i) - rhs.at(i);
55+
sum_squared_error += error * error;
56+
}
57+
58+
return std::sqrt(sum_squared_error / static_cast<double>(count));
59+
}
60+
61+
DynamicSignals calculate_dynamic_signals(
62+
const autoware_planning_msgs::msg::Trajectory & trajectory, const double epsilon)
63+
{
64+
DynamicSignals signals;
65+
const auto num_points = trajectory.points.size();
66+
if (num_points < 2U) {
67+
return signals;
68+
}
69+
70+
signals.accelerations.resize(num_points - 1U, 0.0);
71+
signals.yaw_rates.resize(num_points - 1U, 0.0);
72+
73+
for (std::size_t i = 0; i + 1U < num_points; ++i) {
74+
const auto & point = trajectory.points.at(i);
75+
const auto & next_point = trajectory.points.at(i + 1U);
76+
const double dt = std::max(
77+
get_time_seconds(next_point.time_from_start) - get_time_seconds(point.time_from_start),
78+
epsilon);
79+
80+
const double speed = std::hypot(point.longitudinal_velocity_mps, point.lateral_velocity_mps);
81+
const double next_speed =
82+
std::hypot(next_point.longitudinal_velocity_mps, next_point.lateral_velocity_mps);
83+
signals.accelerations.at(i) = (next_speed - speed) / dt;
84+
85+
const double yaw = tf2::getYaw(point.pose.orientation);
86+
const double next_yaw = tf2::getYaw(next_point.pose.orientation);
87+
const double delta_yaw = autoware_utils_math::normalize_radian(next_yaw - yaw);
88+
signals.yaw_rates.at(i) = delta_yaw / dt;
89+
}
90+
91+
if (signals.accelerations.size() >= 2U) {
92+
signals.jerks.resize(signals.accelerations.size() - 1U, 0.0);
93+
signals.yaw_accelerations.resize(signals.yaw_rates.size() - 1U, 0.0);
94+
95+
for (std::size_t i = 0; i + 1U < signals.accelerations.size(); ++i) {
96+
const auto & point = trajectory.points.at(i);
97+
const auto & next_point = trajectory.points.at(i + 1U);
98+
const double dt = std::max(
99+
get_time_seconds(next_point.time_from_start) - get_time_seconds(point.time_from_start),
100+
epsilon);
101+
signals.jerks.at(i) = (signals.accelerations.at(i + 1U) - signals.accelerations.at(i)) / dt;
102+
signals.yaw_accelerations.at(i) =
103+
(signals.yaw_rates.at(i + 1U) - signals.yaw_rates.at(i)) / dt;
104+
}
105+
}
106+
107+
return signals;
108+
}
109+
110+
bool are_parameters_valid(const ExtendedComfortParameters & parameters)
111+
{
112+
return parameters.max_acceleration_rms >= 0.0 && parameters.max_jerk_rms >= 0.0 &&
113+
parameters.max_yaw_rate_rms >= 0.0 && parameters.max_yaw_acceleration_rms >= 0.0 &&
114+
parameters.finite_difference_epsilon > 0.0;
115+
}
116+
117+
} // namespace
118+
119+
ExtendedComfortResult calculate_extended_comfort(
120+
const autoware_planning_msgs::msg::Trajectory & previous_trajectory,
121+
const autoware_planning_msgs::msg::Trajectory & current_trajectory,
122+
const ExtendedComfortParameters & parameters)
123+
{
124+
if (!are_parameters_valid(parameters)) {
125+
return {0.0, false, "unavailable_invalid_parameters"};
126+
}
127+
128+
if (previous_trajectory.points.size() < 3U || current_trajectory.points.size() < 3U) {
129+
return {0.0, false, "unavailable_short_trajectory"};
130+
}
131+
132+
auto previous_signals =
133+
calculate_dynamic_signals(previous_trajectory, parameters.finite_difference_epsilon);
134+
auto current_signals =
135+
calculate_dynamic_signals(current_trajectory, parameters.finite_difference_epsilon);
136+
137+
if (
138+
previous_signals.accelerations.empty() || current_signals.accelerations.empty() ||
139+
previous_signals.jerks.empty() || current_signals.jerks.empty() ||
140+
previous_signals.yaw_rates.empty() || current_signals.yaw_rates.empty() ||
141+
previous_signals.yaw_accelerations.empty() || current_signals.yaw_accelerations.empty()) {
142+
return {0.0, false, "unavailable_short_trajectory"};
143+
}
144+
145+
const double acceleration_rms =
146+
compute_rms_difference(previous_signals.accelerations, current_signals.accelerations);
147+
const double jerk_rms = compute_rms_difference(previous_signals.jerks, current_signals.jerks);
148+
const double yaw_rate_rms =
149+
compute_rms_difference(previous_signals.yaw_rates, current_signals.yaw_rates);
150+
const double yaw_acceleration_rms =
151+
compute_rms_difference(previous_signals.yaw_accelerations, current_signals.yaw_accelerations);
152+
153+
if (
154+
acceleration_rms > parameters.max_acceleration_rms || jerk_rms > parameters.max_jerk_rms ||
155+
yaw_rate_rms > parameters.max_yaw_rate_rms ||
156+
yaw_acceleration_rms > parameters.max_yaw_acceleration_rms) {
157+
return {0.0, true, "available"};
158+
}
159+
160+
return {1.0, true, "available"};
161+
}
162+
163+
} // namespace autoware::planning_data_analyzer::metrics
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2026 TIER IV, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef METRICS__EXTENDED_COMFORT_HPP_
16+
#define METRICS__EXTENDED_COMFORT_HPP_
17+
18+
#include <autoware_planning_msgs/msg/trajectory.hpp>
19+
20+
#include <string>
21+
22+
namespace autoware::planning_data_analyzer::metrics
23+
{
24+
25+
struct ExtendedComfortParameters
26+
{
27+
double max_acceleration_rms{0.7};
28+
double max_jerk_rms{0.5};
29+
double max_yaw_rate_rms{0.1};
30+
double max_yaw_acceleration_rms{0.1};
31+
double finite_difference_epsilon{1.0e-3};
32+
};
33+
34+
struct ExtendedComfortResult
35+
{
36+
double score{0.0};
37+
bool available{false};
38+
std::string reason{"unavailable"};
39+
};
40+
41+
/**
42+
* @brief Compare consecutive planned trajectories using RMS dynamic-signal discrepancies.
43+
*
44+
* Returns an available binary subscore only when both trajectories are long enough and the
45+
* configured thresholds are valid.
46+
*/
47+
ExtendedComfortResult calculate_extended_comfort(
48+
const autoware_planning_msgs::msg::Trajectory & previous_trajectory,
49+
const autoware_planning_msgs::msg::Trajectory & current_trajectory,
50+
const ExtendedComfortParameters & parameters);
51+
52+
} // namespace autoware::planning_data_analyzer::metrics
53+
54+
#endif // METRICS__EXTENDED_COMFORT_HPP_

planning/autoware_planning_data_analyzer/src/open_loop_evaluator.cpp

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,35 @@ void OpenLoopEvaluator::evaluate(
171171
}
172172

173173
// Evaluate each trajectory with its ground truth
174-
for (const auto & eval_data : evaluation_data_list) {
174+
for (std::size_t i = 0; i < evaluation_data_list.size(); ++i) {
175+
const auto & eval_data = evaluation_data_list.at(i);
175176
// Calculate trajectory point metrics
176177
auto trajectory_metrics = metrics::calculate_trajectory_point_metrics(
177178
eval_data.synchronized_data, route_handler_, history_comfort_params_, lane_keeping_params_,
178179
vehicle_info_);
179180
// Evaluate trajectory
180181
auto metrics = evaluate_trajectory(eval_data);
181182
metrics.history_comfort = trajectory_metrics.history_comfort;
183+
if (i == 0U) {
184+
metrics.extended_comfort = 0.0;
185+
metrics.extended_comfort_available = false;
186+
metrics.extended_comfort_reason = "unavailable_no_previous_trajectory";
187+
} else {
188+
const auto & previous_eval_data = evaluation_data_list.at(i - 1U);
189+
const auto & previous_trajectory = previous_eval_data.synchronized_data->trajectory;
190+
const auto & current_trajectory = eval_data.synchronized_data->trajectory;
191+
if (!previous_trajectory || !current_trajectory) {
192+
metrics.extended_comfort = 0.0;
193+
metrics.extended_comfort_available = false;
194+
metrics.extended_comfort_reason = "unavailable_missing_trajectory";
195+
} else {
196+
const auto extended_comfort_result = metrics::calculate_extended_comfort(
197+
*previous_trajectory, *current_trajectory, extended_comfort_parameters_);
198+
metrics.extended_comfort = extended_comfort_result.score;
199+
metrics.extended_comfort_available = extended_comfort_result.available;
200+
metrics.extended_comfort_reason = extended_comfort_result.reason;
201+
}
202+
}
182203
metrics.lane_keeping = trajectory_metrics.lane_keeping;
183204
metrics.lane_keeping_available = trajectory_metrics.lane_keeping_available;
184205
metrics.lane_keeping_reason = trajectory_metrics.lane_keeping_reason;
@@ -805,15 +826,21 @@ void OpenLoopEvaluator::save_metrics_to_bag(
805826
std_msgs::msg::Float64 scalar_msg;
806827
scalar_msg.data = metrics.history_comfort;
807828
bag_writer.write(scalar_msg, metric_topic("history_comfort"), message_timestamp);
829+
scalar_msg.data = metrics.extended_comfort;
830+
bag_writer.write(scalar_msg, metric_topic("extended_comfort"), message_timestamp);
808831
scalar_msg.data = metrics.lane_keeping;
809832
bag_writer.write(scalar_msg, metric_topic("lane_keeping"), message_timestamp);
810833
scalar_msg.data = metrics.drivable_area_compliance;
811834
bag_writer.write(scalar_msg, metric_topic("drivable_area_compliance"), message_timestamp);
812835
std_msgs::msg::Bool availability_msg;
836+
availability_msg.data = metrics.extended_comfort_available;
837+
bag_writer.write(availability_msg, metric_topic("extended_comfort_available"), message_timestamp);
813838
availability_msg.data = metrics.drivable_area_compliance_available;
814839
bag_writer.write(
815840
availability_msg, metric_topic("drivable_area_compliance_available"), message_timestamp);
816841
std_msgs::msg::String reason_msg;
842+
reason_msg.data = metrics.extended_comfort_reason;
843+
bag_writer.write(reason_msg, metric_topic("extended_comfort_reason"), message_timestamp);
817844
reason_msg.data = metrics.drivable_area_compliance_reason;
818845
bag_writer.write(reason_msg, metric_topic("drivable_area_compliance_reason"), message_timestamp);
819846

@@ -1060,6 +1087,25 @@ nlohmann::json OpenLoopEvaluator::get_summary_as_json() const
10601087
emit_metric(
10611088
"aggregate", "history_comfort", "Binary history comfort subscore across trajectories [-]",
10621089
history_comfort_values);
1090+
std::vector<double> extended_comfort_values;
1091+
extended_comfort_values.reserve(metrics_list_.size());
1092+
std::size_t extended_comfort_available_count = 0;
1093+
std::map<std::string, std::size_t> extended_comfort_reason_counts;
1094+
for (const auto & m : metrics_list_) {
1095+
++extended_comfort_reason_counts[m.extended_comfort_reason];
1096+
if (m.extended_comfort_available) {
1097+
extended_comfort_values.push_back(m.extended_comfort);
1098+
++extended_comfort_available_count;
1099+
}
1100+
}
1101+
emit_metric(
1102+
"aggregate", "extended_comfort",
1103+
"Binary extended comfort subscore across consecutive trajectories [-]",
1104+
extended_comfort_values);
1105+
j["aggregate/extended_comfort_available_count"] = extended_comfort_available_count;
1106+
j["aggregate/extended_comfort_unavailable_count"] =
1107+
metrics_list_.size() - extended_comfort_available_count;
1108+
j["aggregate/extended_comfort_reason_counts"] = extended_comfort_reason_counts;
10631109
std::vector<double> lane_keeping_values;
10641110
lane_keeping_values.reserve(metrics_list_.size());
10651111
std::size_t lane_keeping_available_count = 0;
@@ -1146,6 +1192,9 @@ nlohmann::json OpenLoopEvaluator::get_full_results_as_json() const
11461192
traj["longitudinal_deviations"] = m.longitudinal_deviations;
11471193
traj["ttc"] = m.ttc;
11481194
traj["history_comfort"] = m.history_comfort;
1195+
traj["extended_comfort"] = m.extended_comfort;
1196+
traj["extended_comfort_available"] = m.extended_comfort_available;
1197+
traj["extended_comfort_reason"] = m.extended_comfort_reason;
11491198
traj["lane_keeping"] = m.lane_keeping;
11501199
traj["lane_keeping_available"] = m.lane_keeping_available;
11511200
traj["lane_keeping_reason"] = m.lane_keeping_reason;
@@ -1220,6 +1269,9 @@ std::vector<std::pair<std::string, std::string>> OpenLoopEvaluator::get_result_t
12201269
{metric_topic("fhe"), "std_msgs/msg/Float64MultiArray"},
12211270
{metric_topic("ttc"), "std_msgs/msg/Float64MultiArray"},
12221271
{metric_topic("history_comfort"), "std_msgs/msg/Float64"},
1272+
{metric_topic("extended_comfort"), "std_msgs/msg/Float64"},
1273+
{metric_topic("extended_comfort_available"), "std_msgs/msg/Bool"},
1274+
{metric_topic("extended_comfort_reason"), "std_msgs/msg/String"},
12231275
{metric_topic("lane_keeping"), "std_msgs/msg/Float64"},
12241276
{metric_topic("drivable_area_compliance"), "std_msgs/msg/Float64"},
12251277
{metric_topic("drivable_area_compliance_available"), "std_msgs/msg/Bool"},

0 commit comments

Comments
 (0)