From 21be94c94fc30f19c16a1c6218510f06ddce1fe6 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 30 Jan 2026 19:45:03 +0100 Subject: [PATCH 1/6] Add CLI options for a segmented road --- src/road_gen/main.py | 73 +++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/src/road_gen/main.py b/src/road_gen/main.py index 138e97e..86cd43f 100644 --- a/src/road_gen/main.py +++ b/src/road_gen/main.py @@ -4,9 +4,8 @@ from matplotlib import pyplot as plt from .generators.random_road_generator import RandomRoadGenerator from .generators.segmented_road_generator import SegmentedRoadGenerator - from .plotting.plot_road import plot_road - +from .prefabs import prefabs from .utils import export def add_common_args(parser): @@ -30,48 +29,60 @@ def main(): random_parser.add_argument("--straight_section_max_rel_size", type=float, required=False, help="The maximum size that straight section(s) can have relative to the total length of the path. Defaults to 0.1.") add_common_args(random_parser) + segment_parser = subparsers.add_parser("segments", help="Generate a road according to a list of segments.") + segment_parser.add_argument("--segments", nargs="+", type=str, required=True, help=f"List of segments. Choose from {str(prefabs.PREFABS.keys())}") + segment_parser.add_argument("--alpha", type=float, required=False, help="Dirichlet distribution concentration parameter. A high alpha distributes total length more evenly across segments.") + add_common_args(segment_parser) + args = parser.parse_args() - if args.method == "random": - try: - if not all(v > 0 for v in (args.length, args.ds, args.velocity)): - raise ValueError("Length, step size, and velocity must be positive values.") + try: + if not all(v > 0 for v in (args.length, args.ds, args.velocity)): + raise ValueError("Length, step size, and velocity must be positive values.") - init_args = { - "length": args.length, - "ds": args.ds, - "velocity": args.velocity, - } + init_args = { + "length": args.length, + "ds": args.ds, + "velocity": args.velocity, + } - if args.mu: - init_args["mu"] = args.mu - if args.g: - init_args["g"] = args.g - if args.seed: - init_args["seed"] = args.seed + if args.mu: + init_args["mu"] = args.mu + if args.g: + init_args["g"] = args.g + if args.seed: + init_args["seed"] = args.seed + generate_args = {} + if args.method == "random": generator = RandomRoadGenerator(**init_args) - - generate_args = {} - + if args.straight_section_prob: generate_args["straight_section_prob"] = float(args.straight_section_prob) if args.straight_section_max_rel_size: generate_args["straight_section_max_rel_size"] = float(args.straight_section_max_rel_size) - x, y = generator.generate(**generate_args) + elif args.method == "segments": + generator = SegmentedRoadGenerator(**init_args) - if args.save: - basename = str(generator.seed) - plot_road(x, y, generator, save = True, filename = basename+".jpg") - export.coords_to_json(x, y, filename = basename+".json") - export.params_to_json(generator, filename = basename+".params.json") - else: - plot_road(x, y, generator) + if args.alpha: + generate_args["alpha"] = args.alpha + + generate_args["segments"] = list(args.segments) + + x, y = generator.generate(**generate_args) + + if args.save: + basename = str(generator.seed) + plot_road(x, y, generator, save = True, filename = basename+".jpg") + export.coords_to_json(x, y, filename = basename+".json") + export.params_to_json(generator, filename = basename+".params.json") + else: + plot_road(x, y, generator) - except ValueError as e: - print(f"Error: {e}") - exit(1) + except ValueError as e: + print(f"Error: {e}") + exit(1) if __name__ == "__main__": main() \ No newline at end of file From ddb962569c2273164900c91ff0620cb7ab152cab Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 30 Jan 2026 19:50:48 +0100 Subject: [PATCH 2/6] write test for SegmentedRoadGenerator reproducability --- tests/test_generators.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/test_generators.py b/tests/test_generators.py index 8dae427..7168eb0 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -3,24 +3,42 @@ import numpy as np import pytest from road_gen.generators.random_road_generator import RandomRoadGenerator +from road_gen.generators.segmented_road_generator import SegmentedRoadGenerator @pytest.fixture -def test_params(): +def base_params(): length = 1_000 ds = 10 velocity = 10 return length, ds, velocity +@pytest.fixture +def seg_params(): + segments = ["straight", "turn_left", "straight", "turn_right"] + alpha = 100 + + return segments, alpha -def test_random_road_generator(test_params): + +def test_random_road_generator(base_params): """Test whether fixing the seed for RandomRoadGenerator produces identical output.""" - generator_1 = RandomRoadGenerator(*test_params) + generator_1 = RandomRoadGenerator(*base_params) x1, y1 = generator_1.generate() - generator_2 = RandomRoadGenerator(seed = generator_1.seed, *test_params) + generator_2 = RandomRoadGenerator(seed = generator_1.seed, *base_params) x2, y2 = generator_2.generate() assert np.array_equal(x1, x2) assert np.array_equal(y1, y2) - \ No newline at end of file + +def test_segmented_road_generator(base_params, seg_params): + """Test whether fixing the seed for SegmentedRoadGenerator produces identical output.""" + generator_1 = SegmentedRoadGenerator(*base_params) + x1, y1 = generator_1.generate(*seg_params) + + generator_2 = SegmentedRoadGenerator(seed = generator_1.seed, *base_params) + x2, y2 = generator_2.generate(*seg_params) + + assert np.array_equal(x1, x2) + assert np.array_equal(y1, y2) \ No newline at end of file From 39625e41661bb98e3ea7ccff27cb756531431067 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 30 Jan 2026 19:51:48 +0100 Subject: [PATCH 3/6] fix generic reference to np.random to internal class rng --- src/road_gen/generators/segmented_road_generator.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/road_gen/generators/segmented_road_generator.py b/src/road_gen/generators/segmented_road_generator.py index 26d1b53..4a7fbe5 100644 --- a/src/road_gen/generators/segmented_road_generator.py +++ b/src/road_gen/generators/segmented_road_generator.py @@ -39,9 +39,6 @@ class SegmentedRoadGenerator(BaseRoadGenerator): Args: segments (list[str]): List of segments. - length (int): Total length of the path in meters. - velocity (float): Velocity of the car in km/h. - ds (float, optional): Step size. Defaults to 1.0. 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. Raises: @@ -54,6 +51,8 @@ class SegmentedRoadGenerator(BaseRoadGenerator): if not all(segment in prefabs.PREFABS.keys() for segment in segments): raise ValueError(f"Invalid segment type provided. Available choices: {prefabs.SEGMENTS.keys()}") + self.segments = segments + self.alpha = alpha num_points = int(self.length / self.ds) # divide num_points into len(segments) randomly sized parts. @@ -84,7 +83,7 @@ class SegmentedRoadGenerator(BaseRoadGenerator): if R_min > R_max_angle: raise ValueError("No valid radius for this turn segment") - rand_radius = np.random.uniform(R_min, R_max_angle) + rand_radius = self._rng.uniform(R_min, R_max_angle) if seg_name.startswith("u_turn"): curvature_s = seg_function(rand_radius) From 371d0d1e75a2f7956c36c2d4802cf511f606f6bd Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 30 Jan 2026 19:56:58 +0100 Subject: [PATCH 4/6] Improve documentation --- src/road_gen/generators/segmented_road_generator.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/road_gen/generators/segmented_road_generator.py b/src/road_gen/generators/segmented_road_generator.py index 4a7fbe5..bdacbf1 100644 --- a/src/road_gen/generators/segmented_road_generator.py +++ b/src/road_gen/generators/segmented_road_generator.py @@ -40,13 +40,12 @@ class SegmentedRoadGenerator(BaseRoadGenerator): 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. - + Raises: - ValueError: _description_ - ValueError: _description_ - + ValueError: "No valid radius for this turn segment" means 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: - np.ndarray: _description_ + Tuple[np.ndarray, np.ndarray]: x and y coordinates of the waypoints describing the random road. """ if not all(segment in prefabs.PREFABS.keys() for segment in segments): raise ValueError(f"Invalid segment type provided. Available choices: {prefabs.SEGMENTS.keys()}") From 17719a8865d2dde54416ec1bc8b2727f3e213c6a Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 30 Jan 2026 19:59:34 +0100 Subject: [PATCH 5/6] increase default alpha to help stability under default conditions --- src/road_gen/generators/segmented_road_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/road_gen/generators/segmented_road_generator.py b/src/road_gen/generators/segmented_road_generator.py index bdacbf1..dde5c6f 100644 --- a/src/road_gen/generators/segmented_road_generator.py +++ b/src/road_gen/generators/segmented_road_generator.py @@ -33,7 +33,7 @@ class SegmentedRoadGenerator(BaseRoadGenerator): def generate( self, segments: list[str], - alpha: float = 1.0 + alpha: float = 100 ) -> Tuple[np.ndarray, np.ndarray]: """Generate a curvature profile from a list of segments. From f5313532b13537d31f039f7d02d9214a315af0dd Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 30 Jan 2026 20:05:36 +0100 Subject: [PATCH 6/6] update README to add segmented road example --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4cfc45..c718105 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,20 @@ road-gen random -l 1000 -s 10 -v 10 --save which will produce 3 files starting with the unique seed number used to generate the road. +## Example - segmented road + +A minimal segmented road needs, besides length $L$, step size $\Delta s$ and velocity $v$, a list of segments. + +``` +road-gen segments --segments straight turn_left straight turn_right --length 1000 --ds 10 --velocity 10 +``` + +The parameter $\alpha$ represents the concentration factor for the [Dirichlet distribution](https://numpy.org/doc/2.0/reference/random/generated/numpy.random.dirichlet.html). + ## Reproducability You can reproduce results by adding a seed with the `--seed` flag. ## Other -For more info, just see `road-gen --help` or `road-gen random --help`. \ No newline at end of file +For more info, see `road-gen --help` or `road-gen random --help`. \ No newline at end of file