From 80f7b71c3810e1525d609103eb0efffca9f2de08 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 20 Feb 2026 11:59:09 +0100 Subject: [PATCH] Integrate modules from road-gen that are needed for segmented road generation --- src/road_gen/generators/__init__.py | 0 .../generators/base_road_generator.py | 55 ++++++++ .../generators/segmented_road_generator.py | 120 ++++++++++++++++++ src/road_gen/integrator/__init__.py | 0 src/road_gen/prefabs/__init__.py | 0 src/road_gen/prefabs/prefabs.py | 20 +++ 6 files changed, 195 insertions(+) create mode 100644 src/road_gen/generators/__init__.py create mode 100644 src/road_gen/generators/base_road_generator.py create mode 100644 src/road_gen/generators/segmented_road_generator.py create mode 100644 src/road_gen/integrator/__init__.py create mode 100644 src/road_gen/prefabs/__init__.py create mode 100644 src/road_gen/prefabs/prefabs.py diff --git a/src/road_gen/generators/__init__.py b/src/road_gen/generators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/road_gen/generators/base_road_generator.py b/src/road_gen/generators/base_road_generator.py new file mode 100644 index 0000000..ac40fc2 --- /dev/null +++ b/src/road_gen/generators/base_road_generator.py @@ -0,0 +1,55 @@ +import secrets + +import numpy as np + + +class BaseRoadGenerator: + """A base generator object for generating a road of a specified length.""" + def __init__( + self, + length: int | float, + ds: int | float, + velocity: int | float, + mu: float = 0.7, + g: float = 9.81, + seed: int | None = None + ): + """Initialize a BaseGenerator with a given or random seed. + + Args: + length (int | float): The total length of the road in meters. + ds (int | float): The step size in meters. + velocity (int | float): Velocity in meters per second. + mu (float): Coefficient of friction. Defaults to 0.7 (dry asphalt). + g (float): Acceleration due to gravity (m/s^2). Defaults to 9.81. + seed (int | None, optional): Set a seed for the generator. + Defaults to a random seed. + """ + if seed is None: + seed = secrets.randbits(32) + + if not isinstance(seed, int): + raise TypeError("seed must be an integer or None.") + + if not isinstance(length, int | float): + raise TypeError("Length must be an integer or float in meters.") + + if not isinstance(ds, int | float): + raise TypeError("Step size must be integer or float in meters.") + + if not isinstance(velocity, int | float): + raise TypeError( + "Velocity must be integer or float in meters per second." + ) + + self.length = length + self.ds = ds + + self.velocity = velocity + self.min_radius = (velocity ** 2) / (g * mu) + + self.seed = seed + self._rng = np.random.default_rng(seed) + + def generate(self): + pass diff --git a/src/road_gen/generators/segmented_road_generator.py b/src/road_gen/generators/segmented_road_generator.py new file mode 100644 index 0000000..351d27f --- /dev/null +++ b/src/road_gen/generators/segmented_road_generator.py @@ -0,0 +1,120 @@ +from typing import Tuple + +import numpy as np + +from .base_road_generator import BaseRoadGenerator +from road_gen.prefabs import prefabs +from road_gen.integrator.integrator import integrate_road + + +class SegmentedRoadGenerator(BaseRoadGenerator): + def __init__( + self, + length: int | float, + ds: int | float, + velocity: int | float, + mu: float = 0.7, + g: float = 9.81, + seed: int | None = None + ): + """Initialize a SegmentedRoadGenerator with given or random seed. + + Args: + length (int | float): The total length of the road in meters. + ds (int | float): The step size in meters. + velocity (int | float): Velocity in meters per second. + mu (float): Coefficient of friction. Defaults to 0.7 (dry asphalt). + g (float): Acceleration due to gravity (m/s^2). Defaults to 9.81. + seed (int | None, optional): Set a seed for the generator. + Defaults to random seed. + """ + + super().__init__(length, ds, velocity, mu, g, seed) + + def generate( + self, + segments: list[str], + alpha: float = 100, + min_turn_angle: float = 15., + max_turn_angle: float = 90. + ) -> Tuple[np.ndarray, np.ndarray]: + """Generate a curvature profile from a list of segments. + + Args: + segments (list[str]): List of segments. + alpha (float, optional): Dirichlet concentration parameter. + A higher value leads to more uniform apportionment of the + length amongst the segments, while a lower value allows more + random apportionment. Defaults to 1.0. + min_turn_angle (float, optional): Minimum turn angle in degrees for + random sampling of turn radius. Does nothing if `angle_list` is + provided or no `turn_*` segement is specified in the `segments` + list. + min_turn_angle (float, optional): Maximum turn angle in degrees for + random sampling of turn radius. Does nothing if `angle_list` is + provided or no `turn_*` segement is specified in the `segments` + list. + Raises: + ValueError: Raised when a turn + is too tight given its segment length and the velocity. + To fix this, you can try to reduce the amount of segments or + increase length. Increasing alpha + (Dirichlet concentration parameter) can also help because this + reduces the odds of very small lengths being assigned to + turn segments. + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y coordinates of the + waypoints describing the random road. + """ + existing_prefabs = prefabs.PREFABS.keys() + if not all(segment in existing_prefabs for segment in segments): + raise ValueError( + "Invalid segment type provided. Available choices" + f"{existing_prefabs}" + ) + + self.segments = segments + self.alpha = alpha + num_points = int(self.length / self.ds) + + # divide num_points into len(segments) randomly sized parts. + parts = self._rng.dirichlet(np.full(len(segments), alpha), size=1)[0] + parts = parts * num_points + parts = np.round(parts).astype(int) + + # correct round off so the sum of parts is still total length L. + if sum(parts) != num_points: + parts[0] += num_points - sum(parts) + + curvature = np.zeros(num_points) + current_index = 0 + + for seg_name, seg_length in zip(segments, parts): + seg_function = prefabs.PREFABS[seg_name] + + if seg_name == 'straight': + curvature_s = seg_function(seg_length) + else: + R_min_angle = seg_length / np.deg2rad(max_turn_angle) + R_max_angle = seg_length / np.deg2rad(min_turn_angle) + + # physics limit + R_min = max(self.min_radius, R_min_angle) + + if R_min > R_max_angle: + raise ValueError("No valid radius for this turn segment") + + rand_radius = self._rng.uniform(R_min, R_max_angle) + + if seg_name.startswith("u_turn"): + curvature_s = seg_function(rand_radius) + else: + curvature_s = seg_function(seg_length, rand_radius) + + curvature[current_index:(current_index + seg_length)] = curvature_s + current_index += seg_length + + x, y = integrate_road(curvature) + + return x * self.ds, y * self.ds diff --git a/src/road_gen/integrator/__init__.py b/src/road_gen/integrator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/road_gen/prefabs/__init__.py b/src/road_gen/prefabs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/road_gen/prefabs/prefabs.py b/src/road_gen/prefabs/prefabs.py new file mode 100644 index 0000000..6f1cb79 --- /dev/null +++ b/src/road_gen/prefabs/prefabs.py @@ -0,0 +1,20 @@ +import numpy as np + + +def straight(length: int) -> np.ndarray: + return np.zeros(length) + + +def turn_left(length: int, radius: float) -> np.ndarray: + return np.full(length, 1.0 / radius) + + +def turn_right(length: int, radius: float) -> np.ndarray: + return -turn_left(length, radius) + + +PREFABS = { + 'straight': straight, + 'turn_left': turn_left, + 'turn_right': turn_right, +}