Add YAML configuration file support for VectorMap
Enable loading algorithm parameters from YAML files for reproducible experiments and easier parameter tuning without recompilation. - Add yaml.rs module with from_yaml_file/to_yaml_file methods - Add Serialize/Deserialize derives to all config structs - Add finalize() method to recompute derived fields after deserialization - Add configs/default.yaml reference configuration with documentation - Update test harness to load config from VASTU_MAP_CONFIG env var - Move serde/serde_yaml to main dependencies
This commit is contained in:
@@ -7,24 +7,24 @@ license = "MIT"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
integration-tests = ["dep:sangam-io", "dep:svg", "dep:image", "dep:serde_yaml"]
|
||||
integration-tests = ["dep:sangam-io", "dep:svg", "dep:image"]
|
||||
|
||||
[dependencies]
|
||||
rstar = "0.12" # R-tree spatial index
|
||||
log = "0.4" # Logging
|
||||
rand = "0.9" # RANSAC randomness
|
||||
serde = { version = "1.0", features = ["derive"] } # Config serialization
|
||||
serde_yaml = "0.9" # YAML config files
|
||||
|
||||
# Integration test dependencies (optional)
|
||||
sangam-io = { path = "../sangam-io", features = ["mock"], optional = true }
|
||||
svg = { version = "0.18", optional = true }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "pnm"], optional = true }
|
||||
serde_yaml = { version = "0.9", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.8" # Benchmarks
|
||||
env_logger = "0.11" # Test logging
|
||||
approx = "0.5" # Approximate comparisons (tests only)
|
||||
serde = { version = "1.0", features = ["derive"] } # Test config parsing
|
||||
|
||||
[[test]]
|
||||
name = "integration"
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# VastuMap YAML Configuration
|
||||
# All values must be specified (no preset inheritance)
|
||||
# Units: meters (m), radians (rad), counts (n)
|
||||
|
||||
# ============================================================================
|
||||
# Line Extraction (Split-Merge Algorithm)
|
||||
# ============================================================================
|
||||
extraction:
|
||||
# Maximum perpendicular distance for splitting (m)
|
||||
split_threshold: 0.05
|
||||
# Minimum points to form a valid line
|
||||
min_points: 5
|
||||
# Minimum line length to keep (m)
|
||||
min_length: 0.10
|
||||
# Maximum gap between consecutive points (m)
|
||||
max_point_gap: 0.30
|
||||
# Merge threshold for collinear segments (m)
|
||||
merge_threshold: 0.05
|
||||
# Coefficient for range-adaptive threshold scaling
|
||||
adaptive_range_scale: 0.03
|
||||
|
||||
# ============================================================================
|
||||
# RANSAC Line Extraction (for hybrid mode)
|
||||
# ============================================================================
|
||||
ransac_extraction:
|
||||
# Number of RANSAC iterations per line
|
||||
iterations: 100
|
||||
# Inlier distance threshold (m)
|
||||
inlier_threshold: 0.02
|
||||
# Minimum inliers for valid line
|
||||
min_inliers: 5
|
||||
# Minimum line length (m)
|
||||
min_line_length: 0.1
|
||||
# Maximum gap between inliers (m) - prevents connecting walls across doorways
|
||||
max_point_gap: 0.10
|
||||
# Maximum lines to extract (0 = unlimited)
|
||||
max_lines: 0
|
||||
# Random seed (0 = random)
|
||||
seed: 0
|
||||
|
||||
# Whether to use RANSAC + Split-Merge hybrid extraction
|
||||
use_hybrid_extraction: true
|
||||
|
||||
# ============================================================================
|
||||
# Corner Detection
|
||||
# ============================================================================
|
||||
corner:
|
||||
# Minimum angle between lines to form corner (rad)
|
||||
min_angle: 0.524 # 30 degrees
|
||||
# Maximum angle between lines to form corner (rad)
|
||||
max_angle: 2.618 # 150 degrees
|
||||
# Maximum endpoint distance for corner detection (m)
|
||||
max_endpoint_distance: 0.05
|
||||
# Curvature detection window size (points on each side)
|
||||
curvature_window_size: 3
|
||||
# Minimum curvature angle for corner candidate (rad)
|
||||
curvature_threshold: 0.785 # 45 degrees
|
||||
# Merge distance for deduplicating corners (m)
|
||||
merge_distance: 0.10
|
||||
# Non-maximum suppression radius (m)
|
||||
nms_radius: 0.15
|
||||
|
||||
# ============================================================================
|
||||
# ICP Scan Matching
|
||||
# ============================================================================
|
||||
matching:
|
||||
# Maximum ICP iterations
|
||||
max_iterations: 30
|
||||
# Convergence threshold for pose change
|
||||
convergence_threshold: 0.0001
|
||||
# Maximum correspondence distance (m)
|
||||
max_correspondence_distance: 0.5
|
||||
|
||||
# Robust cost function for outlier handling
|
||||
# Options: None, Huber, Cauchy, Tukey, GemanMcClure
|
||||
robust_cost:
|
||||
type: Huber
|
||||
delta: 0.03
|
||||
|
||||
# Use Iteratively Reweighted Least Squares
|
||||
use_irls: true
|
||||
# Minimum correspondences required
|
||||
min_correspondences: 10
|
||||
# Minimum match confidence to accept
|
||||
min_confidence: 0.3
|
||||
# Use RANSAC for initial pose estimate
|
||||
use_ransac_init: true
|
||||
# Use batch (SIMD) correspondence search
|
||||
use_batch_search: true
|
||||
|
||||
# Lidar noise model
|
||||
noise_model:
|
||||
sigma_base: 0.01
|
||||
sigma_range: 0.001
|
||||
|
||||
# Coarse search when confidence is low
|
||||
use_coarse_search: true
|
||||
coarse_search_confidence_threshold: 0.5
|
||||
|
||||
# Multi-resolution ICP (coarse-to-fine)
|
||||
multi_resolution:
|
||||
enabled: true
|
||||
levels: 3
|
||||
subsample_factor: 2
|
||||
iterations_per_level: [3, 5, 10]
|
||||
|
||||
# ============================================================================
|
||||
# Line Association (Map Integration)
|
||||
# ============================================================================
|
||||
association:
|
||||
# Maximum perpendicular distance for association (m)
|
||||
max_perpendicular_distance: 0.15
|
||||
# Maximum angular difference (rad)
|
||||
max_angle_difference: 0.2
|
||||
# Minimum overlap ratio for valid association
|
||||
min_overlap_ratio: 0.3
|
||||
# Maximum gap between line endpoints (m)
|
||||
max_endpoint_gap: 0.3
|
||||
# Weight for distance in quality scoring
|
||||
distance_weight: 10.0
|
||||
# Weight for angle in quality scoring
|
||||
angle_weight: 5.0
|
||||
|
||||
# ============================================================================
|
||||
# Line Merging
|
||||
# ============================================================================
|
||||
merger:
|
||||
# Weight given to existing map line (0-1)
|
||||
map_weight: 0.8
|
||||
# Whether to extend line endpoints
|
||||
extend_endpoints: true
|
||||
# Maximum extension per observation (m)
|
||||
max_extension: 0.5
|
||||
# Minimum extension to apply (m)
|
||||
min_extension: 0.05
|
||||
|
||||
# ============================================================================
|
||||
# Frontier Detection
|
||||
# ============================================================================
|
||||
frontier:
|
||||
# Maximum endpoint distance for "connected" (m)
|
||||
connection_distance: 0.3
|
||||
# Minimum distance from robot for valid frontier (m)
|
||||
min_distance_from_robot: 0.5
|
||||
# Minimum openness score for valid frontier
|
||||
min_openness: 0.3
|
||||
|
||||
# ============================================================================
|
||||
# Occupancy Queries
|
||||
# ============================================================================
|
||||
occupancy:
|
||||
# Distance threshold for "occupied" (m)
|
||||
obstacle_distance: 0.1
|
||||
# Distance threshold for "known" (m)
|
||||
known_distance: 2.0
|
||||
# Use winding number for inside/outside determination
|
||||
use_winding_number: false
|
||||
|
||||
# ============================================================================
|
||||
# Path Planning
|
||||
# ============================================================================
|
||||
path_planning:
|
||||
# Robot radius for collision checking (m)
|
||||
robot_radius: 0.15
|
||||
# Maximum visibility graph nodes
|
||||
max_nodes: 1000
|
||||
# Only plan through known free space
|
||||
conservative: true
|
||||
|
||||
# ============================================================================
|
||||
# Loop Closure Detection
|
||||
# ============================================================================
|
||||
loop_closure:
|
||||
# Minimum travel distance before checking loop closure (m)
|
||||
min_travel_distance: 5.0
|
||||
# Maximum descriptor distance for candidates
|
||||
max_descriptor_distance: 0.5
|
||||
# Minimum ICP confidence to accept closure
|
||||
min_verification_confidence: 0.6
|
||||
# Distance between keyframes (m)
|
||||
keyframe_interval: 2.0
|
||||
# Radius for computing descriptors (m)
|
||||
descriptor_radius: 2.0
|
||||
# Maximum candidates to verify per keyframe
|
||||
max_candidates_to_verify: 3
|
||||
# Minimum lines for valid keyframe
|
||||
min_lines_for_keyframe: 3
|
||||
# Minimum corners for valid keyframe
|
||||
min_corners_for_keyframe: 2
|
||||
|
||||
# ICP config for loop closure verification (nested)
|
||||
icp_config:
|
||||
max_iterations: 30
|
||||
convergence_threshold: 0.0001
|
||||
max_correspondence_distance: 0.5
|
||||
robust_cost:
|
||||
type: Huber
|
||||
delta: 0.03
|
||||
use_irls: true
|
||||
min_correspondences: 10
|
||||
min_confidence: 0.3
|
||||
use_ransac_init: true
|
||||
use_batch_search: true
|
||||
noise_model:
|
||||
sigma_base: 0.01
|
||||
sigma_range: 0.001
|
||||
use_coarse_search: true
|
||||
coarse_search_confidence_threshold: 0.5
|
||||
multi_resolution:
|
||||
enabled: true
|
||||
levels: 3
|
||||
subsample_factor: 2
|
||||
iterations_per_level: [3, 5, 10]
|
||||
|
||||
# ============================================================================
|
||||
# Global Settings
|
||||
# ============================================================================
|
||||
# Minimum match confidence to use scan matching (vs. odometry)
|
||||
min_match_confidence: 0.3
|
||||
|
||||
# Whether to update map with new observations
|
||||
mapping_enabled: true
|
||||
|
||||
# Whether loop closure detection is enabled
|
||||
loop_closure_enabled: true
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! All configuration parameters have sensible defaults for indoor robot navigation.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// Lidar measurement noise model.
|
||||
@@ -11,7 +12,7 @@ use std::f32::consts::PI;
|
||||
///
|
||||
/// This allows weighting correspondences by their measurement quality.
|
||||
/// Close-range measurements are more reliable than far-range ones.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct LidarNoiseModel {
|
||||
/// Base measurement standard deviation (meters).
|
||||
/// Minimum uncertainty even at zero range.
|
||||
@@ -135,7 +136,7 @@ impl LidarNoiseModel {
|
||||
///
|
||||
/// map.enable_exploration_mode(config);
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ExplorationConfig {
|
||||
/// Re-fit lines every N observations.
|
||||
/// Default: 10
|
||||
@@ -215,7 +216,7 @@ impl ExplorationConfig {
|
||||
///
|
||||
/// All parameters have sensible defaults. Create with `Default::default()`
|
||||
/// and modify as needed.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct VectorMapConfig {
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Input Filtering
|
||||
|
||||
@@ -23,12 +23,14 @@
|
||||
//! let corners = detect_hybrid_corners(&points, &lines, &corner_config);
|
||||
//! ```
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::Point2D;
|
||||
use crate::features::{Corner2D, Line2D};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// Configuration for corner detection.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CornerConfig {
|
||||
/// Minimum angle between lines to form a corner (radians).
|
||||
/// Lines that are nearly parallel (angle below this) don't form corners.
|
||||
|
||||
@@ -61,7 +61,7 @@ use crate::features::Line2D;
|
||||
///
|
||||
/// // Lines longer than 1m will be split into smaller segments
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LineFitConfig {
|
||||
/// Maximum length for a single line segment.
|
||||
///
|
||||
@@ -73,14 +73,6 @@ pub struct LineFitConfig {
|
||||
pub max_line_length: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for LineFitConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_line_length: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LineFitConfig {
|
||||
/// Create a new configuration with default settings.
|
||||
pub fn new() -> Self {
|
||||
@@ -126,7 +118,7 @@ pub fn split_line_by_length(line: &Line2D, max_length: f32) -> Vec<Line2D> {
|
||||
let length = line.length();
|
||||
|
||||
if length <= max_length || max_length <= 0.0 {
|
||||
return vec![line.clone()];
|
||||
return vec![*line];
|
||||
}
|
||||
|
||||
// Calculate number of segments needed
|
||||
@@ -157,16 +149,14 @@ pub fn split_line_by_length(line: &Line2D, max_length: f32) -> Vec<Line2D> {
|
||||
// Use exact endpoint for last segment to avoid floating point drift
|
||||
line.end
|
||||
} else {
|
||||
Point2D::new(
|
||||
line.start.x + dir.x * t_end,
|
||||
line.start.y + dir.y * t_end,
|
||||
)
|
||||
Point2D::new(line.start.x + dir.x * t_end, line.start.y + dir.y * t_end)
|
||||
};
|
||||
|
||||
// Each segment inherits observation_count=1, proportional point_count
|
||||
let segment_point_count = if i == num_segments - 1 {
|
||||
// Last segment gets remaining points
|
||||
line.point_count.saturating_sub(points_per_segment * (num_segments - 1) as u32)
|
||||
line.point_count
|
||||
.saturating_sub(points_per_segment * (num_segments - 1) as u32)
|
||||
} else {
|
||||
points_per_segment
|
||||
};
|
||||
@@ -1180,11 +1170,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_split_line_short_line_no_split() {
|
||||
let line = Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(0.5, 0.0),
|
||||
10,
|
||||
);
|
||||
let line = Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(0.5, 0.0), 10);
|
||||
|
||||
let segments = split_line_by_length(&line, 1.0);
|
||||
|
||||
@@ -1196,11 +1182,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_split_line_exact_max_length() {
|
||||
let line = Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(1.0, 0.0),
|
||||
10,
|
||||
);
|
||||
let line = Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0), 10);
|
||||
|
||||
let segments = split_line_by_length(&line, 1.0);
|
||||
|
||||
@@ -1209,11 +1191,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_split_line_into_two() {
|
||||
let line = Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(2.0, 0.0),
|
||||
20,
|
||||
);
|
||||
let line = Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(2.0, 0.0), 20);
|
||||
|
||||
let segments = split_line_by_length(&line, 1.0);
|
||||
|
||||
@@ -1232,11 +1210,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_split_line_into_multiple() {
|
||||
let line = Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(5.0, 0.0),
|
||||
50,
|
||||
);
|
||||
let line = Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(5.0, 0.0), 50);
|
||||
|
||||
let segments = split_line_by_length(&line, 1.0);
|
||||
|
||||
@@ -1254,11 +1228,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_split_line_diagonal() {
|
||||
// Diagonal line of length 5*sqrt(2) ≈ 7.07
|
||||
let line = Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(5.0, 5.0),
|
||||
30,
|
||||
);
|
||||
let line = Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(5.0, 5.0), 30);
|
||||
|
||||
let segments = split_line_by_length(&line, 2.0);
|
||||
|
||||
@@ -1279,11 +1249,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_split_line_zero_max_length() {
|
||||
let line = Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(2.0, 0.0),
|
||||
10,
|
||||
);
|
||||
let line = Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(2.0, 0.0), 10);
|
||||
|
||||
// Zero or negative max_length should return original line
|
||||
let segments = split_line_by_length(&line, 0.0);
|
||||
@@ -1318,17 +1284,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_line_fit_config_builder() {
|
||||
let config = LineFitConfig::new()
|
||||
.with_max_line_length(1.5);
|
||||
let config = LineFitConfig::new().with_max_line_length(1.5);
|
||||
|
||||
assert_eq!(config.max_line_length, Some(1.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fit_line_with_config_no_split() {
|
||||
let points: Vec<Point2D> = (0..10)
|
||||
.map(|i| Point2D::new(i as f32 * 0.1, 0.0))
|
||||
.collect();
|
||||
let points: Vec<Point2D> = (0..10).map(|i| Point2D::new(i as f32 * 0.1, 0.0)).collect();
|
||||
|
||||
// Config with no max length
|
||||
let config = LineFitConfig::new();
|
||||
@@ -1340,9 +1303,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_fit_line_with_config_with_split() {
|
||||
// Create a 5m horizontal line
|
||||
let points: Vec<Point2D> = (0..51)
|
||||
.map(|i| Point2D::new(i as f32 * 0.1, 0.0))
|
||||
.collect();
|
||||
let points: Vec<Point2D> = (0..51).map(|i| Point2D::new(i as f32 * 0.1, 0.0)).collect();
|
||||
|
||||
// Config with 1m max length
|
||||
let config = LineFitConfig::new().with_max_line_length(1.0);
|
||||
@@ -1353,7 +1314,11 @@ mod tests {
|
||||
|
||||
// Each segment should be approximately 1m
|
||||
for line in &lines {
|
||||
assert!(line.length() <= 1.01, "Line length {} > 1.01", line.length());
|
||||
assert!(
|
||||
line.length() <= 1.01,
|
||||
"Line length {} > 1.01",
|
||||
line.length()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1371,14 +1336,18 @@ mod tests {
|
||||
fn test_fit_line_from_sensor_with_config() {
|
||||
let sensor_pos = Point2D::new(0.0, 0.0);
|
||||
let points: Vec<Point2D> = (10..40)
|
||||
.map(|i| Point2D::new(i as f32 * 0.1, 1.0)) // 3m wall at y=1
|
||||
.map(|i| Point2D::new(i as f32 * 0.1, 1.0)) // 3m wall at y=1
|
||||
.collect();
|
||||
|
||||
let config = LineFitConfig::new().with_max_line_length(0.5);
|
||||
let lines = fit_line_from_sensor_with_config(&points, sensor_pos, None, &config);
|
||||
|
||||
// 3m line should split into 6 segments of ~0.5m
|
||||
assert!(lines.len() >= 5, "Expected >= 5 segments, got {}", lines.len());
|
||||
assert!(
|
||||
lines.len() >= 5,
|
||||
"Expected >= 5 segments, got {}",
|
||||
lines.len()
|
||||
);
|
||||
|
||||
// All segments should be approximately horizontal
|
||||
for line in &lines {
|
||||
@@ -1392,9 +1361,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_fit_line_weighted_with_config() {
|
||||
let points: Vec<Point2D> = (0..31)
|
||||
.map(|i| Point2D::new(i as f32 * 0.1, 0.0))
|
||||
.collect();
|
||||
let points: Vec<Point2D> = (0..31).map(|i| Point2D::new(i as f32 * 0.1, 0.0)).collect();
|
||||
let weights: Vec<f32> = vec![1.0; 31];
|
||||
|
||||
let config = LineFitConfig::new().with_max_line_length(1.0);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
//! let (lines2, remaining2) = extract_lines_ransac_with_scratch(&points2, &config, &mut scratch);
|
||||
//! ```
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::Point2D;
|
||||
use crate::features::Line2D;
|
||||
|
||||
@@ -148,7 +150,7 @@ impl Default for RansacScratchSpace {
|
||||
}
|
||||
|
||||
/// Configuration for RANSAC line extraction.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RansacLineConfig {
|
||||
/// Number of RANSAC iterations per line.
|
||||
/// More iterations = better chance of finding optimal line.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Configuration for Split-and-Merge line extraction.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Configuration for Split-and-Merge algorithm.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SplitMergeConfig {
|
||||
/// Maximum perpendicular distance for a point to be considered on the line.
|
||||
/// If any point deviates more than this, the segment is split.
|
||||
|
||||
@@ -72,12 +72,7 @@ impl Line2D {
|
||||
|
||||
/// Create a line segment with all fields specified.
|
||||
#[inline]
|
||||
pub fn full(
|
||||
start: Point2D,
|
||||
end: Point2D,
|
||||
observation_count: u32,
|
||||
point_count: u32,
|
||||
) -> Self {
|
||||
pub fn full(start: Point2D, end: Point2D, observation_count: u32, point_count: u32) -> Self {
|
||||
Self {
|
||||
start,
|
||||
end,
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
//! - Angular similarity
|
||||
//! - Overlap extent
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::math::angle_diff;
|
||||
use crate::features::Line2D;
|
||||
|
||||
use super::spatial_index::SpatialIndex;
|
||||
|
||||
/// Configuration for line association.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AssociationConfig {
|
||||
/// Maximum perpendicular distance for association (meters).
|
||||
/// Default: 0.15m
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
//! Merges associated lines into updated map lines, extending
|
||||
//! endpoints and updating parameters based on observations.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::Point2D;
|
||||
use crate::features::Line2D;
|
||||
|
||||
use super::association::Association;
|
||||
|
||||
/// Configuration for line merging.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MergerConfig {
|
||||
/// Weight given to existing map line (vs. scan line).
|
||||
/// Higher values make the map more stable but slower to adapt.
|
||||
@@ -70,7 +72,7 @@ impl MergerConfig {
|
||||
///
|
||||
/// Used by [`optimize_coplanar_lines`] to merge multiple lines
|
||||
/// that lie on the same infinite line (within tolerances).
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CoplanarMergeConfig {
|
||||
/// Maximum angle difference for lines to be considered coplanar (radians).
|
||||
/// Default: 0.052 rad (~3°)
|
||||
@@ -96,9 +98,9 @@ pub struct CoplanarMergeConfig {
|
||||
impl Default for CoplanarMergeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_angle_diff: 0.052, // ~3 degrees
|
||||
max_perpendicular_dist: 0.05, // 5cm
|
||||
max_gap: 0.3, // 30cm
|
||||
max_angle_diff: 0.052, // ~3 degrees
|
||||
max_perpendicular_dist: 0.05, // 5cm
|
||||
max_gap: 0.3, // 30cm
|
||||
min_dominant_point_count: 10,
|
||||
}
|
||||
}
|
||||
@@ -576,8 +578,7 @@ pub fn optimize_coplanar_lines(lines: &mut Vec<Line2D>, config: &CoplanarMergeCo
|
||||
}
|
||||
|
||||
// Collect groups
|
||||
let mut groups: std::collections::HashMap<usize, Vec<usize>> =
|
||||
std::collections::HashMap::new();
|
||||
let mut groups: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
|
||||
for i in 0..n {
|
||||
let root = find(&mut parent, i);
|
||||
groups.entry(root).or_default().push(i);
|
||||
@@ -862,11 +863,7 @@ mod tests {
|
||||
Point2D::new(3.0, 0.0),
|
||||
15, // Above threshold
|
||||
),
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(2.0, 0.0),
|
||||
Point2D::new(5.0, 0.0),
|
||||
12,
|
||||
),
|
||||
Line2D::with_point_count(Point2D::new(2.0, 0.0), Point2D::new(5.0, 0.0), 12),
|
||||
];
|
||||
let config = CoplanarMergeConfig::default();
|
||||
|
||||
@@ -881,11 +878,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_optimize_coplanar_lines_merges_with_gap() {
|
||||
let mut lines = vec![
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(2.0, 0.0),
|
||||
12,
|
||||
),
|
||||
Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(2.0, 0.0), 12),
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(2.2, 0.0), // 0.2m gap, within default 0.3m
|
||||
Point2D::new(5.0, 0.0),
|
||||
@@ -930,11 +923,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_optimize_coplanar_lines_respects_max_gap() {
|
||||
let mut lines = vec![
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(2.0, 0.0),
|
||||
15,
|
||||
),
|
||||
Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(2.0, 0.0), 15),
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(4.0, 0.0), // 2.0m gap, above default 0.3m
|
||||
Point2D::new(6.0, 0.0),
|
||||
@@ -977,27 +966,11 @@ mod tests {
|
||||
// Two separate walls (horizontal and vertical)
|
||||
let mut lines = vec![
|
||||
// Horizontal wall (will merge)
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(0.0, 0.0),
|
||||
Point2D::new(2.0, 0.0),
|
||||
15,
|
||||
),
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(2.0, 0.0),
|
||||
Point2D::new(4.0, 0.0),
|
||||
12,
|
||||
),
|
||||
Line2D::with_point_count(Point2D::new(0.0, 0.0), Point2D::new(2.0, 0.0), 15),
|
||||
Line2D::with_point_count(Point2D::new(2.0, 0.0), Point2D::new(4.0, 0.0), 12),
|
||||
// Vertical wall (will merge)
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(5.0, 0.0),
|
||||
Point2D::new(5.0, 2.0),
|
||||
15,
|
||||
),
|
||||
Line2D::with_point_count(
|
||||
Point2D::new(5.0, 2.0),
|
||||
Point2D::new(5.0, 4.0),
|
||||
12,
|
||||
),
|
||||
Line2D::with_point_count(Point2D::new(5.0, 0.0), Point2D::new(5.0, 2.0), 15),
|
||||
Line2D::with_point_count(Point2D::new(5.0, 2.0), Point2D::new(5.0, 4.0), 12),
|
||||
];
|
||||
let config = CoplanarMergeConfig::default();
|
||||
|
||||
|
||||
@@ -171,20 +171,21 @@ pub fn associate_scan_points(
|
||||
let mut total_unassociated = 0usize;
|
||||
|
||||
for &world_point in points {
|
||||
if let Some((line_idx, distance)) = index.nearest_line(world_point) {
|
||||
if distance <= config.max_distance && line_idx < num_lines {
|
||||
let line = &lines[line_idx];
|
||||
let t = line.project_point(world_point);
|
||||
if line.contains_projection_extended(t, config.max_projection_extension) {
|
||||
line_points[line_idx].push(AssociatedPoint {
|
||||
point: world_point,
|
||||
scan_id,
|
||||
distance,
|
||||
projection_t: t,
|
||||
});
|
||||
total_associated += 1;
|
||||
continue;
|
||||
}
|
||||
if let Some((line_idx, distance)) = index.nearest_line(world_point)
|
||||
&& distance <= config.max_distance
|
||||
&& line_idx < num_lines
|
||||
{
|
||||
let line = &lines[line_idx];
|
||||
let t = line.project_point(world_point);
|
||||
if line.contains_projection_extended(t, config.max_projection_extension) {
|
||||
line_points[line_idx].push(AssociatedPoint {
|
||||
point: world_point,
|
||||
scan_id,
|
||||
distance,
|
||||
projection_t: t,
|
||||
});
|
||||
total_associated += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
total_unassociated += 1;
|
||||
@@ -213,10 +214,7 @@ pub fn lines_with_enough_points(result: &PointAssociationResult, min_points: usi
|
||||
/// Extract just the points for a specific line (without metadata).
|
||||
///
|
||||
/// Useful when only the point coordinates are needed for fitting.
|
||||
pub fn extract_points_for_line(
|
||||
result: &PointAssociationResult,
|
||||
line_idx: usize,
|
||||
) -> Vec<Point2D> {
|
||||
pub fn extract_points_for_line(result: &PointAssociationResult, line_idx: usize) -> Vec<Point2D> {
|
||||
result
|
||||
.line_points
|
||||
.get(line_idx)
|
||||
@@ -291,9 +289,9 @@ mod tests {
|
||||
|
||||
// Points close to line 0 (y=0)
|
||||
let store = make_scan_store_with_points(&[
|
||||
(2.0, 0.05), // Very close to y=0
|
||||
(5.0, 0.02), // Very close to y=0
|
||||
(8.0, 4.98), // Very close to y=5
|
||||
(2.0, 0.05), // Very close to y=0
|
||||
(5.0, 0.02), // Very close to y=0
|
||||
(8.0, 4.98), // Very close to y=5
|
||||
]);
|
||||
|
||||
let result = associate_points_to_lines(&store, &lines, &index, &config);
|
||||
@@ -311,8 +309,8 @@ mod tests {
|
||||
|
||||
// Points too far from line
|
||||
let store = make_scan_store_with_points(&[
|
||||
(5.0, 0.5), // 0.5m away, too far
|
||||
(5.0, 1.0), // 1.0m away, too far
|
||||
(5.0, 0.5), // 0.5m away, too far
|
||||
(5.0, 1.0), // 1.0m away, too far
|
||||
]);
|
||||
|
||||
let result = associate_points_to_lines(&store, &lines, &index, &config);
|
||||
@@ -332,9 +330,9 @@ mod tests {
|
||||
|
||||
// Points beyond line endpoints
|
||||
let store = make_scan_store_with_points(&[
|
||||
(-2.0, 0.0), // Before start
|
||||
(12.0, 0.0), // After end
|
||||
(5.0, 0.0), // On segment (should associate)
|
||||
(-2.0, 0.0), // Before start
|
||||
(12.0, 0.0), // After end
|
||||
(5.0, 0.0), // On segment (should associate)
|
||||
]);
|
||||
|
||||
let result = associate_points_to_lines(&store, &lines, &index, &config);
|
||||
@@ -354,9 +352,9 @@ mod tests {
|
||||
.with_max_projection_extension(0.1);
|
||||
|
||||
let store = make_scan_store_with_points(&[
|
||||
(-0.5, 0.0), // 5% before start, within extension
|
||||
(10.5, 0.0), // 5% after end, within extension
|
||||
(-2.0, 0.0), // 20% before start, outside extension
|
||||
(-0.5, 0.0), // 5% before start, within extension
|
||||
(10.5, 0.0), // 5% after end, within extension
|
||||
(-2.0, 0.0), // 20% before start, outside extension
|
||||
]);
|
||||
|
||||
let result = associate_points_to_lines(&store, &lines, &index, &config);
|
||||
@@ -459,9 +457,9 @@ mod tests {
|
||||
let config = PointAssociationConfig::default().with_max_distance(0.5);
|
||||
|
||||
let points = vec![
|
||||
Point2D::new(5.0, 0.05), // Near line 0
|
||||
Point2D::new(9.95, 5.0), // Near line 1
|
||||
Point2D::new(5.0, 5.0), // Far from both
|
||||
Point2D::new(5.0, 0.05), // Near line 0
|
||||
Point2D::new(9.95, 5.0), // Near line 1
|
||||
Point2D::new(5.0, 5.0), // Far from both
|
||||
];
|
||||
|
||||
let result = associate_scan_points(&points, 42, &lines, &index, &config);
|
||||
@@ -512,11 +510,7 @@ mod tests {
|
||||
let index = SpatialIndex::new(&lines);
|
||||
let config = PointAssociationConfig::default().with_max_distance(0.5);
|
||||
|
||||
let store = make_scan_store_with_points(&[
|
||||
(2.0, 0.05),
|
||||
(5.0, 0.05),
|
||||
(8.0, 0.05),
|
||||
]);
|
||||
let store = make_scan_store_with_points(&[(2.0, 0.05), (5.0, 0.05), (8.0, 0.05)]);
|
||||
|
||||
let result = associate_points_to_lines(&store, &lines, &index, &config);
|
||||
let points = extract_points_for_line(&result, 0);
|
||||
@@ -534,10 +528,10 @@ mod tests {
|
||||
let config = PointAssociationConfig::default().with_max_distance(0.1);
|
||||
|
||||
let store = make_scan_store_with_points(&[
|
||||
(5.0, 0.05), // Close enough
|
||||
(5.0, 0.5), // Too far
|
||||
(5.0, 0.08), // Close enough
|
||||
(5.0, 1.0), // Too far
|
||||
(5.0, 0.05), // Close enough
|
||||
(5.0, 0.5), // Too far
|
||||
(5.0, 0.08), // Close enough
|
||||
(5.0, 1.0), // Too far
|
||||
]);
|
||||
|
||||
let result = associate_points_to_lines(&store, &lines, &index, &config);
|
||||
|
||||
@@ -80,7 +80,7 @@ impl RefitResult {
|
||||
/// Get all lines from the result.
|
||||
pub fn lines(&self) -> Vec<Line2D> {
|
||||
match self {
|
||||
RefitResult::Single(line) => vec![line.clone()],
|
||||
RefitResult::Single(line) => vec![*line],
|
||||
RefitResult::Split(lines) => lines.clone(),
|
||||
RefitResult::Insufficient => vec![],
|
||||
}
|
||||
@@ -262,8 +262,7 @@ fn split_and_fit(
|
||||
|
||||
// Fit each segment
|
||||
for (range_start, range_end) in ranges {
|
||||
let segment_points: Vec<&AssociatedPoint> =
|
||||
sorted_points[range_start..range_end].to_vec();
|
||||
let segment_points: Vec<&AssociatedPoint> = sorted_points[range_start..range_end].to_vec();
|
||||
|
||||
if segment_points.len() < config.min_points {
|
||||
continue;
|
||||
@@ -340,11 +339,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_horizontal_line(y: f32, x_start: f32, x_end: f32) -> Line2D {
|
||||
Line2D::with_observation_count(
|
||||
Point2D::new(x_start, y),
|
||||
Point2D::new(x_end, y),
|
||||
5,
|
||||
)
|
||||
Line2D::with_observation_count(Point2D::new(x_start, y), Point2D::new(x_end, y), 5)
|
||||
}
|
||||
|
||||
fn make_associated_point(x: f32, y: f32, scan_id: u32, t: f32) -> AssociatedPoint {
|
||||
@@ -374,7 +369,12 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Create a dense point cloud (no gaps > threshold).
|
||||
fn make_dense_points(x_start: f32, x_end: f32, y: f32, gap_threshold: f32) -> Vec<AssociatedPoint> {
|
||||
fn make_dense_points(
|
||||
x_start: f32,
|
||||
x_end: f32,
|
||||
y: f32,
|
||||
gap_threshold: f32,
|
||||
) -> Vec<AssociatedPoint> {
|
||||
let line_length = (x_end - x_start).abs();
|
||||
// Create points with spacing less than gap_threshold
|
||||
let spacing = gap_threshold * 0.5;
|
||||
|
||||
@@ -591,9 +591,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_test_points(n: usize) -> Vec<Point2D> {
|
||||
(0..n)
|
||||
.map(|i| Point2D::new(i as f32 * 0.1, 0.0))
|
||||
.collect()
|
||||
(0..n).map(|i| Point2D::new(i as f32 * 0.1, 0.0)).collect()
|
||||
}
|
||||
|
||||
fn make_point_cloud(n: usize) -> PointCloud2D {
|
||||
|
||||
@@ -126,7 +126,7 @@ pub mod vector_map;
|
||||
|
||||
// Re-export main types at crate root
|
||||
pub use motion_model::MotionModel;
|
||||
pub use vector_map::{ConfigError, VectorMap, VectorMapConfig};
|
||||
pub use vector_map::{ConfigError, VectorMap, VectorMapConfig, YamlConfigError};
|
||||
|
||||
// Re-export exploration types
|
||||
pub use exploration::{
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::{Point2D, Pose2D};
|
||||
use crate::features::{Corner2D, Line2D, ScanDescriptor};
|
||||
use crate::matching::{IcpConfig, match_scan};
|
||||
@@ -38,7 +40,7 @@ use crate::matching::{IcpConfig, match_scan};
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Configuration for loop closure detection.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct LoopClosureConfig {
|
||||
/// Minimum travel distance before considering loop closure (meters).
|
||||
/// Prevents detecting loops immediately after keyframe creation.
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//! Configuration for Point-to-Line ICP algorithm.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::LidarNoiseModel;
|
||||
|
||||
use super::super::robust::RobustCostFunction;
|
||||
|
||||
/// Configuration for Point-to-Line ICP.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct IcpConfig {
|
||||
/// Maximum number of ICP iterations.
|
||||
/// Default: 30
|
||||
@@ -18,6 +20,7 @@ pub struct IcpConfig {
|
||||
|
||||
/// Pre-computed squared convergence threshold for efficient comparison.
|
||||
/// Avoids sqrt() in hot loop by comparing squared distances.
|
||||
#[serde(skip)]
|
||||
pub(crate) convergence_threshold_sq: f32,
|
||||
|
||||
/// Maximum correspondence distance (meters).
|
||||
@@ -99,6 +102,13 @@ impl IcpConfig {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Recompute derived fields after deserialization.
|
||||
///
|
||||
/// Call this after loading from YAML to ensure computed fields are correct.
|
||||
pub fn finalize(&mut self) {
|
||||
self.convergence_threshold_sq = self.convergence_threshold * self.convergence_threshold;
|
||||
}
|
||||
|
||||
// ===== Preset Configurations =====
|
||||
|
||||
/// Fast preset: Optimized for speed at the cost of some accuracy.
|
||||
@@ -287,7 +297,7 @@ impl IcpConfig {
|
||||
}
|
||||
|
||||
/// Configuration for coarse search initialization.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CoarseSearchConfig {
|
||||
/// Maximum translation search range (meters).
|
||||
/// Search from -range to +range in both x and y.
|
||||
@@ -330,7 +340,7 @@ impl Default for CoarseSearchConfig {
|
||||
/// Multi-resolution ICP improves convergence by starting with subsampled
|
||||
/// points (coarse) and progressively refining with more points (fine).
|
||||
/// This helps escape local minima when the initial guess is poor.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MultiResolutionConfig {
|
||||
/// Enable multi-resolution matching.
|
||||
/// Default: true
|
||||
|
||||
@@ -24,11 +24,14 @@
|
||||
//! | Tukey | Zero weight for outliers | Aggressive rejection |
|
||||
//! | Geman-McClure | Redescending | Multimodal errors |
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Robust cost function for weighted least squares (IRLS).
|
||||
///
|
||||
/// Computes weights that down-weight outliers based on residual magnitude.
|
||||
/// Use with ICP to improve accuracy in presence of noise and clutter.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum RobustCostFunction {
|
||||
/// No robust weighting (standard least squares).
|
||||
/// Weight is always 1.0 regardless of residual.
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
//! let frontiers = detector.detect(&new_lines, &config);
|
||||
//! ```
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::Frontier;
|
||||
@@ -257,7 +258,7 @@ impl FrontierDetector {
|
||||
}
|
||||
|
||||
/// Configuration for frontier detection.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct FrontierConfig {
|
||||
/// Maximum distance for endpoints to be considered "connected" (meters).
|
||||
/// Endpoints closer than this are not frontiers.
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
//! Determines whether a point is in free space, occupied, or unknown
|
||||
//! based on the map's line features.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Occupancy;
|
||||
use crate::core::{Bounds, Point2D};
|
||||
use crate::features::Line2D;
|
||||
use crate::integration::SpatialIndex;
|
||||
|
||||
/// Configuration for occupancy queries.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OccupancyConfig {
|
||||
/// Distance threshold for considering a point "on" a line (meters).
|
||||
/// Points within this distance of a line are considered occupied.
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::{Bounds, Point2D};
|
||||
use crate::features::Line2D;
|
||||
use crate::query::occupancy::OccupancyConfig;
|
||||
@@ -36,7 +38,7 @@ use std::cmp::Ordering;
|
||||
use std::collections::BinaryHeap;
|
||||
|
||||
/// Configuration for path planning.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PathPlanningConfig {
|
||||
/// Robot radius for obstacle inflation (meters).
|
||||
/// Paths will avoid coming within this distance of obstacles.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Configuration for VectorMap.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::extraction::{CornerConfig, RansacLineConfig, SplitMergeConfig};
|
||||
use crate::integration::{AssociationConfig, MergerConfig};
|
||||
use crate::loop_closure::LoopClosureConfig;
|
||||
@@ -11,7 +13,7 @@ use crate::query::{FrontierConfig, OccupancyConfig, PathPlanningConfig};
|
||||
// ============================================================================
|
||||
|
||||
/// Errors that can occur during configuration validation.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ConfigError {
|
||||
/// A threshold value is invalid (negative or zero when positive required).
|
||||
InvalidThreshold {
|
||||
@@ -83,7 +85,7 @@ impl std::fmt::Display for ConfigError {
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
/// Configuration for VectorMap.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct VectorMapConfig {
|
||||
/// Configuration for line extraction (split-merge).
|
||||
pub extraction: SplitMergeConfig,
|
||||
@@ -464,6 +466,14 @@ impl VectorMapConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recompute derived fields after deserialization.
|
||||
///
|
||||
/// Call this after loading from YAML to ensure computed fields are correct.
|
||||
pub fn finalize(&mut self) {
|
||||
self.matching.finalize();
|
||||
self.loop_closure.icp_config.finalize();
|
||||
}
|
||||
|
||||
/// Validate and return self for builder pattern.
|
||||
///
|
||||
/// # Panics
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::core::{Bounds, Point2D, PointCloud2D, Pose2D};
|
||||
use crate::extraction::{detect_corners, extract_lines, extract_lines_hybrid};
|
||||
use crate::features::{Corner2D, FeatureSet, Line2D};
|
||||
use crate::integration::{
|
||||
PointAssociationConfig, RefitConfig, RefitResult, RefitStats, ScanStore, ScanStoreConfig,
|
||||
associate_points_to_lines, batch_merge, create_new_line, find_associations,
|
||||
find_unmatched_scan_lines, refit_line, PointAssociationConfig, RefitConfig, RefitResult,
|
||||
RefitStats, ScanStore, ScanStoreConfig,
|
||||
find_unmatched_scan_lines, refit_line,
|
||||
};
|
||||
use crate::loop_closure::{LoopClosure, LoopClosureDetector};
|
||||
use crate::matching::PointToLineIcp;
|
||||
@@ -420,9 +420,7 @@ impl VectorMap {
|
||||
// ─── Scan Storage (Exploration Mode) ───
|
||||
if let Some(ref mut store) = self.scan_store {
|
||||
store.add_scan(
|
||||
odometry,
|
||||
final_pose,
|
||||
scan,
|
||||
odometry, final_pose, scan,
|
||||
0, // features_extracted - will be updated after extraction
|
||||
confidence,
|
||||
);
|
||||
@@ -440,15 +438,16 @@ impl VectorMap {
|
||||
};
|
||||
|
||||
// ─── Periodic Line Re-fitting (Exploration Mode) ───
|
||||
if let Some(ref config) = self.exploration_config {
|
||||
if config.refit_interval > 0 && self.observation_count % config.refit_interval == 0 {
|
||||
log::debug!(
|
||||
"Triggering line re-fit at observation {}",
|
||||
self.observation_count
|
||||
);
|
||||
// Trigger line re-fitting from accumulated scans
|
||||
let _ = self.refit_lines_from_scans();
|
||||
}
|
||||
if let Some(ref config) = self.exploration_config
|
||||
&& config.refit_interval > 0
|
||||
&& self.observation_count.is_multiple_of(config.refit_interval)
|
||||
{
|
||||
log::debug!(
|
||||
"Triggering line re-fit at observation {}",
|
||||
self.observation_count
|
||||
);
|
||||
// Trigger line re-fitting from accumulated scans
|
||||
let _ = self.refit_lines_from_scans();
|
||||
}
|
||||
|
||||
// ─── Loop Closure ───
|
||||
@@ -503,10 +502,8 @@ impl VectorMap {
|
||||
// ─── Scan Storage (Exploration Mode) ───
|
||||
if let Some(ref mut store) = self.scan_store {
|
||||
store.add_scan(
|
||||
odometry,
|
||||
odometry, // First scan: odometry = estimated pose
|
||||
scan,
|
||||
0, // features_extracted - unknown at this point
|
||||
odometry, odometry, // First scan: odometry = estimated pose
|
||||
scan, 0, // features_extracted - unknown at this point
|
||||
0.0, // confidence - no ICP for first scan
|
||||
);
|
||||
}
|
||||
@@ -674,11 +671,9 @@ impl VectorMap {
|
||||
/// let merged = map.optimize_lines(&config);
|
||||
/// println!("Merged {} redundant lines", merged);
|
||||
/// ```
|
||||
pub fn optimize_lines(
|
||||
&mut self,
|
||||
config: &crate::integration::CoplanarMergeConfig,
|
||||
) -> usize {
|
||||
let merged = crate::integration::optimize_coplanar_lines(self.line_store.lines_mut(), config);
|
||||
pub fn optimize_lines(&mut self, config: &crate::integration::CoplanarMergeConfig) -> usize {
|
||||
let merged =
|
||||
crate::integration::optimize_coplanar_lines(self.line_store.lines_mut(), config);
|
||||
|
||||
if merged > 0 {
|
||||
// Rebuild auxiliary structures
|
||||
@@ -763,12 +758,7 @@ impl VectorMap {
|
||||
for (line_idx, associated_points) in associations.line_points.iter().enumerate() {
|
||||
let original = &self.line_store.lines()[line_idx];
|
||||
|
||||
let result = refit_line(
|
||||
associated_points,
|
||||
original,
|
||||
&robot_poses,
|
||||
&refit_config,
|
||||
);
|
||||
let result = refit_line(associated_points, original, &robot_poses, &refit_config);
|
||||
|
||||
match result {
|
||||
RefitResult::Single(line) => {
|
||||
@@ -782,7 +772,7 @@ impl VectorMap {
|
||||
}
|
||||
RefitResult::Insufficient => {
|
||||
// Keep original line
|
||||
new_lines.push(original.clone());
|
||||
new_lines.push(*original);
|
||||
stats.lines_insufficient += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@
|
||||
mod config;
|
||||
mod line_store;
|
||||
mod map;
|
||||
mod yaml;
|
||||
|
||||
// Re-export public API
|
||||
pub use config::{ConfigError, VectorMapConfig};
|
||||
pub use line_store::LineStore;
|
||||
pub use map::VectorMap;
|
||||
pub use yaml::YamlConfigError;
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
//! YAML configuration file loading for VectorMap.
|
||||
//!
|
||||
//! Load algorithm configuration from YAML files for reproducible experiments
|
||||
//! and easy parameter tuning.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use vastu_map::VectorMapConfig;
|
||||
//!
|
||||
//! // Load from file
|
||||
//! let config = VectorMapConfig::from_yaml_file("config.yaml")?;
|
||||
//!
|
||||
//! // Validate before use
|
||||
//! config.validate()?;
|
||||
//!
|
||||
//! let map = VectorMap::new(config);
|
||||
//! ```
|
||||
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::vector_map::config::{ConfigError, VectorMapConfig};
|
||||
|
||||
/// Errors that can occur when loading YAML configuration.
|
||||
#[derive(Debug)]
|
||||
pub enum YamlConfigError {
|
||||
/// IO error reading the file.
|
||||
Io(io::Error),
|
||||
/// YAML parsing error.
|
||||
Parse(serde_yaml::Error),
|
||||
/// Configuration validation failed.
|
||||
Validation(ConfigError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for YamlConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
YamlConfigError::Io(e) => write!(f, "IO error: {}", e),
|
||||
YamlConfigError::Parse(e) => write!(f, "YAML parse error: {}", e),
|
||||
YamlConfigError::Validation(e) => write!(f, "Config validation error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for YamlConfigError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
YamlConfigError::Io(e) => Some(e),
|
||||
YamlConfigError::Parse(e) => Some(e),
|
||||
YamlConfigError::Validation(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for YamlConfigError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
YamlConfigError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_yaml::Error> for YamlConfigError {
|
||||
fn from(err: serde_yaml::Error) -> Self {
|
||||
YamlConfigError::Parse(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigError> for YamlConfigError {
|
||||
fn from(err: ConfigError) -> Self {
|
||||
YamlConfigError::Validation(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl VectorMapConfig {
|
||||
/// Load configuration from a YAML file.
|
||||
///
|
||||
/// Parses the file and validates the configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path` - Path to the YAML configuration file
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `YamlConfigError` if the file cannot be read, parsed, or validated.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
/// let config = VectorMapConfig::from_yaml_file("config.yaml")?;
|
||||
/// let map = VectorMap::new(config);
|
||||
/// ```
|
||||
pub fn from_yaml_file<P: AsRef<Path>>(path: P) -> Result<Self, YamlConfigError> {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Self::from_yaml_str(&contents)
|
||||
}
|
||||
|
||||
/// Load configuration from a YAML string.
|
||||
///
|
||||
/// Parses the string and validates the configuration.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
/// let yaml = r#"
|
||||
/// extraction:
|
||||
/// split_threshold: 0.05
|
||||
/// "#;
|
||||
/// let config = VectorMapConfig::from_yaml_str(yaml)?;
|
||||
/// ```
|
||||
pub fn from_yaml_str(yaml: &str) -> Result<Self, YamlConfigError> {
|
||||
let mut config: VectorMapConfig = serde_yaml::from_str(yaml)?;
|
||||
|
||||
// Finalize derived fields (like convergence_threshold_sq)
|
||||
config.finalize();
|
||||
|
||||
// Validate configuration
|
||||
config.validate()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Load configuration from a YAML file without validation.
|
||||
///
|
||||
/// Use this if you want to manually validate or modify the config after loading.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
/// let mut config = VectorMapConfig::from_yaml_file_unchecked("config.yaml")?;
|
||||
/// config.matching.max_iterations = 50; // Modify
|
||||
/// config.validate()?; // Manually validate
|
||||
/// ```
|
||||
pub fn from_yaml_file_unchecked<P: AsRef<Path>>(path: P) -> Result<Self, YamlConfigError> {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Self::from_yaml_str_unchecked(&contents)
|
||||
}
|
||||
|
||||
/// Load configuration from a YAML string without validation.
|
||||
pub fn from_yaml_str_unchecked(yaml: &str) -> Result<Self, YamlConfigError> {
|
||||
let mut config: VectorMapConfig = serde_yaml::from_str(yaml)?;
|
||||
config.finalize();
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Serialize configuration to a YAML string.
|
||||
///
|
||||
/// Useful for saving configurations or generating templates.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
/// let config = VectorMapConfig::default();
|
||||
/// let yaml = config.to_yaml_string()?;
|
||||
/// println!("{}", yaml);
|
||||
/// ```
|
||||
pub fn to_yaml_string(&self) -> Result<String, serde_yaml::Error> {
|
||||
serde_yaml::to_string(self)
|
||||
}
|
||||
|
||||
/// Save configuration to a YAML file.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
/// let config = VectorMapConfig::default();
|
||||
/// config.to_yaml_file("reference.yaml")?;
|
||||
/// ```
|
||||
pub fn to_yaml_file<P: AsRef<Path>>(&self, path: P) -> Result<(), YamlConfigError> {
|
||||
let yaml = self.to_yaml_string()?;
|
||||
std::fs::write(path, yaml)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_default_config() {
|
||||
let original = VectorMapConfig::default();
|
||||
let yaml = original.to_yaml_string().expect("Failed to serialize");
|
||||
let loaded = VectorMapConfig::from_yaml_str(&yaml).expect("Failed to deserialize");
|
||||
|
||||
// Key fields should match
|
||||
assert_eq!(
|
||||
original.matching.max_iterations,
|
||||
loaded.matching.max_iterations
|
||||
);
|
||||
assert_eq!(original.use_hybrid_extraction, loaded.use_hybrid_extraction);
|
||||
assert_eq!(original.mapping_enabled, loaded.mapping_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validation_called_on_load() {
|
||||
// Create invalid YAML with zero iterations
|
||||
let yaml = r#"
|
||||
extraction:
|
||||
split_threshold: 0.05
|
||||
min_points: 5
|
||||
min_length: 0.1
|
||||
max_point_gap: 0.3
|
||||
merge_threshold: 0.05
|
||||
adaptive_range_scale: 0.03
|
||||
ransac_extraction:
|
||||
iterations: 0
|
||||
inlier_threshold: 0.02
|
||||
min_inliers: 5
|
||||
min_line_length: 0.1
|
||||
max_point_gap: 0.1
|
||||
max_lines: 0
|
||||
seed: 0
|
||||
use_hybrid_extraction: true
|
||||
corner:
|
||||
min_angle: 0.524
|
||||
max_angle: 2.618
|
||||
max_endpoint_distance: 0.05
|
||||
curvature_window_size: 3
|
||||
curvature_threshold: 0.785
|
||||
merge_distance: 0.1
|
||||
nms_radius: 0.15
|
||||
matching:
|
||||
max_iterations: 30
|
||||
convergence_threshold: 0.0001
|
||||
max_correspondence_distance: 0.5
|
||||
robust_cost:
|
||||
type: Huber
|
||||
delta: 0.03
|
||||
use_irls: true
|
||||
min_correspondences: 10
|
||||
min_confidence: 0.3
|
||||
use_ransac_init: true
|
||||
use_batch_search: true
|
||||
noise_model:
|
||||
sigma_base: 0.01
|
||||
sigma_range: 0.001
|
||||
use_coarse_search: true
|
||||
coarse_search_confidence_threshold: 0.5
|
||||
multi_resolution:
|
||||
enabled: true
|
||||
levels: 3
|
||||
subsample_factor: 2
|
||||
iterations_per_level: [3, 5, 10]
|
||||
association:
|
||||
max_perpendicular_distance: 0.15
|
||||
max_angle_difference: 0.2
|
||||
min_overlap_ratio: 0.3
|
||||
max_endpoint_gap: 0.3
|
||||
distance_weight: 10.0
|
||||
angle_weight: 5.0
|
||||
merger:
|
||||
map_weight: 0.8
|
||||
extend_endpoints: true
|
||||
max_extension: 0.5
|
||||
min_extension: 0.05
|
||||
frontier:
|
||||
connection_distance: 0.3
|
||||
min_distance_from_robot: 0.5
|
||||
min_openness: 0.3
|
||||
occupancy:
|
||||
obstacle_distance: 0.1
|
||||
known_distance: 2.0
|
||||
use_winding_number: false
|
||||
path_planning:
|
||||
robot_radius: 0.15
|
||||
max_nodes: 1000
|
||||
conservative: true
|
||||
loop_closure:
|
||||
min_travel_distance: 5.0
|
||||
max_descriptor_distance: 0.5
|
||||
min_verification_confidence: 0.6
|
||||
keyframe_interval: 2.0
|
||||
descriptor_radius: 2.0
|
||||
max_candidates_to_verify: 3
|
||||
min_lines_for_keyframe: 3
|
||||
min_corners_for_keyframe: 2
|
||||
icp_config:
|
||||
max_iterations: 30
|
||||
convergence_threshold: 0.0001
|
||||
max_correspondence_distance: 0.5
|
||||
robust_cost:
|
||||
type: Huber
|
||||
delta: 0.03
|
||||
use_irls: true
|
||||
min_correspondences: 10
|
||||
min_confidence: 0.3
|
||||
use_ransac_init: true
|
||||
use_batch_search: true
|
||||
noise_model:
|
||||
sigma_base: 0.01
|
||||
sigma_range: 0.001
|
||||
use_coarse_search: true
|
||||
coarse_search_confidence_threshold: 0.5
|
||||
multi_resolution:
|
||||
enabled: true
|
||||
levels: 3
|
||||
subsample_factor: 2
|
||||
iterations_per_level: [3, 5, 10]
|
||||
min_match_confidence: 0.3
|
||||
mapping_enabled: true
|
||||
loop_closure_enabled: true
|
||||
"#;
|
||||
|
||||
let result = VectorMapConfig::from_yaml_str(yaml);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
YamlConfigError::Validation(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unchecked_load_skips_validation() {
|
||||
// Same invalid YAML but using unchecked method
|
||||
let yaml = r#"
|
||||
extraction:
|
||||
split_threshold: 0.05
|
||||
min_points: 5
|
||||
min_length: 0.1
|
||||
max_point_gap: 0.3
|
||||
merge_threshold: 0.05
|
||||
adaptive_range_scale: 0.03
|
||||
ransac_extraction:
|
||||
iterations: 0
|
||||
inlier_threshold: 0.02
|
||||
min_inliers: 5
|
||||
min_line_length: 0.1
|
||||
max_point_gap: 0.1
|
||||
max_lines: 0
|
||||
seed: 0
|
||||
use_hybrid_extraction: true
|
||||
corner:
|
||||
min_angle: 0.524
|
||||
max_angle: 2.618
|
||||
max_endpoint_distance: 0.05
|
||||
curvature_window_size: 3
|
||||
curvature_threshold: 0.785
|
||||
merge_distance: 0.1
|
||||
nms_radius: 0.15
|
||||
matching:
|
||||
max_iterations: 30
|
||||
convergence_threshold: 0.0001
|
||||
max_correspondence_distance: 0.5
|
||||
robust_cost:
|
||||
type: Huber
|
||||
delta: 0.03
|
||||
use_irls: true
|
||||
min_correspondences: 10
|
||||
min_confidence: 0.3
|
||||
use_ransac_init: true
|
||||
use_batch_search: true
|
||||
noise_model:
|
||||
sigma_base: 0.01
|
||||
sigma_range: 0.001
|
||||
use_coarse_search: true
|
||||
coarse_search_confidence_threshold: 0.5
|
||||
multi_resolution:
|
||||
enabled: true
|
||||
levels: 3
|
||||
subsample_factor: 2
|
||||
iterations_per_level: [3, 5, 10]
|
||||
association:
|
||||
max_perpendicular_distance: 0.15
|
||||
max_angle_difference: 0.2
|
||||
min_overlap_ratio: 0.3
|
||||
max_endpoint_gap: 0.3
|
||||
distance_weight: 10.0
|
||||
angle_weight: 5.0
|
||||
merger:
|
||||
map_weight: 0.8
|
||||
extend_endpoints: true
|
||||
max_extension: 0.5
|
||||
min_extension: 0.05
|
||||
frontier:
|
||||
connection_distance: 0.3
|
||||
min_distance_from_robot: 0.5
|
||||
min_openness: 0.3
|
||||
occupancy:
|
||||
obstacle_distance: 0.1
|
||||
known_distance: 2.0
|
||||
use_winding_number: false
|
||||
path_planning:
|
||||
robot_radius: 0.15
|
||||
max_nodes: 1000
|
||||
conservative: true
|
||||
loop_closure:
|
||||
min_travel_distance: 5.0
|
||||
max_descriptor_distance: 0.5
|
||||
min_verification_confidence: 0.6
|
||||
keyframe_interval: 2.0
|
||||
descriptor_radius: 2.0
|
||||
max_candidates_to_verify: 3
|
||||
min_lines_for_keyframe: 3
|
||||
min_corners_for_keyframe: 2
|
||||
icp_config:
|
||||
max_iterations: 30
|
||||
convergence_threshold: 0.0001
|
||||
max_correspondence_distance: 0.5
|
||||
robust_cost:
|
||||
type: Huber
|
||||
delta: 0.03
|
||||
use_irls: true
|
||||
min_correspondences: 10
|
||||
min_confidence: 0.3
|
||||
use_ransac_init: true
|
||||
use_batch_search: true
|
||||
noise_model:
|
||||
sigma_base: 0.01
|
||||
sigma_range: 0.001
|
||||
use_coarse_search: true
|
||||
coarse_search_confidence_threshold: 0.5
|
||||
multi_resolution:
|
||||
enabled: true
|
||||
levels: 3
|
||||
subsample_factor: 2
|
||||
iterations_per_level: [3, 5, 10]
|
||||
min_match_confidence: 0.3
|
||||
mapping_enabled: true
|
||||
loop_closure_enabled: true
|
||||
"#;
|
||||
|
||||
// Should succeed with unchecked method
|
||||
let result = VectorMapConfig::from_yaml_str_unchecked(yaml);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// But validation should fail
|
||||
let config = result.unwrap();
|
||||
assert!(config.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_yaml_fails() {
|
||||
let result = VectorMapConfig::from_yaml_str("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_io_error_conversion() {
|
||||
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
|
||||
let yaml_err: YamlConfigError = io_err.into();
|
||||
assert!(matches!(yaml_err, YamlConfigError::Io(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_default_yaml_file() {
|
||||
// Load the reference config file from configs/default.yaml
|
||||
let config_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("configs")
|
||||
.join("default.yaml");
|
||||
|
||||
let config = VectorMapConfig::from_yaml_file(&config_path)
|
||||
.expect("Failed to load configs/default.yaml");
|
||||
|
||||
// Verify key values match expected defaults
|
||||
assert_eq!(config.matching.max_iterations, 30);
|
||||
assert!(config.use_hybrid_extraction);
|
||||
assert!(config.mapping_enabled);
|
||||
assert!(config.loop_closure_enabled);
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,24 @@
|
||||
//!
|
||||
//! Provides synchronous simulation using sangam-io's mock device components
|
||||
//! directly (without spawning daemon threads).
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! SLAM algorithm parameters can be customized via YAML configuration:
|
||||
//!
|
||||
//! - Set `VASTU_MAP_CONFIG` environment variable to specify a custom config file
|
||||
//! - Falls back to `configs/default.yaml` in the project root
|
||||
//! - Falls back to `VectorMapConfig::default()` if no YAML file is found
|
||||
//!
|
||||
//! ```bash
|
||||
//! # Use custom config
|
||||
//! VASTU_MAP_CONFIG=my_config.yaml cargo test --features integration-tests
|
||||
//!
|
||||
//! # Use default config file
|
||||
//! cargo test --features integration-tests
|
||||
//! ```
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use sangam_io::devices::mock::config::{LidarConfig, RobotConfig};
|
||||
use sangam_io::devices::mock::lidar_sim::LidarSimulator;
|
||||
@@ -55,6 +73,56 @@ pub struct HarnessConfig {
|
||||
pub fast_mode: bool,
|
||||
}
|
||||
|
||||
/// Environment variable name for custom SLAM config file path.
|
||||
const SLAM_CONFIG_ENV_VAR: &str = "VASTU_MAP_CONFIG";
|
||||
|
||||
/// Load VectorMapConfig from YAML file.
|
||||
///
|
||||
/// Priority:
|
||||
/// 1. `VASTU_MAP_CONFIG` environment variable (if set)
|
||||
/// 2. `configs/default.yaml` in project root
|
||||
/// 3. `VectorMapConfig::default()` as fallback
|
||||
fn load_slam_config() -> VectorMapConfig {
|
||||
// Check environment variable first
|
||||
if let Ok(config_path) = std::env::var(SLAM_CONFIG_ENV_VAR) {
|
||||
match VectorMapConfig::from_yaml_file(&config_path) {
|
||||
Ok(config) => {
|
||||
log::info!("Loaded SLAM config from env var: {}", config_path);
|
||||
return config;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to load SLAM config from {}: {}, using default",
|
||||
config_path,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try default config file in project root
|
||||
let default_config_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("configs/default.yaml");
|
||||
if default_config_path.exists() {
|
||||
match VectorMapConfig::from_yaml_file(&default_config_path) {
|
||||
Ok(config) => {
|
||||
log::info!("Loaded SLAM config from: {}", default_config_path.display());
|
||||
return config;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to load default SLAM config from {}: {}, using builtin defaults",
|
||||
default_config_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to builtin defaults
|
||||
log::info!("Using builtin VectorMapConfig::default()");
|
||||
VectorMapConfig::default()
|
||||
}
|
||||
|
||||
impl Default for HarnessConfig {
|
||||
fn default() -> Self {
|
||||
// Use lidar config with zero offsets for test accuracy
|
||||
@@ -75,7 +143,7 @@ impl Default for HarnessConfig {
|
||||
start_theta: 0.0,
|
||||
robot: RobotConfig::default(),
|
||||
lidar,
|
||||
slam: VectorMapConfig::default(),
|
||||
slam: load_slam_config(),
|
||||
random_seed: 42, // Fixed seed for reproducibility
|
||||
min_quality: 50,
|
||||
min_range: 0.15,
|
||||
@@ -126,8 +194,8 @@ impl HarnessConfig {
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/integration/scenarios/maps/medium_room.yaml"
|
||||
);
|
||||
// Use non-conservative path planning for exploration
|
||||
let slam = VectorMapConfig::default()
|
||||
// Load base config from YAML, then override path planning for exploration
|
||||
let slam = load_slam_config()
|
||||
.with_path_planning(PathPlanningConfig::default().with_conservative(false));
|
||||
Self {
|
||||
map_file: map_file.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user