mirror of
https://github.com/pim-n/road-gen.git
synced 2026-02-03 09:23:09 +01:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36f4abb425 | |||
| 784f22bd63 | |||
| 3b7c37f7c4 | |||
| 8a1fad0b94 | |||
| 0a6aa4a7fc | |||
| 2fde08cf21 | |||
| 79cda5f094 | |||
| 46fe5d559d | |||
| 7dfe5140c6 | |||
| 563afb2fe9 | |||
| 602d0d08d0 | |||
| f5313532b1 | |||
| 17719a8865 | |||
| 371d0d1e75 | |||
| 39625e4166 | |||
| ddb962569c | |||
| 21be94c94f |
14
README.md
14
README.md
@ -54,10 +54,22 @@ 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`.
|
||||
For more info, see `road-gen --help` or `road-gen random --help`.
|
||||
|
||||
There are some Jupyter notebooks explaining [roads as planar curves](docs/planar_curve.ipynb), as well as the actual implementation of [random segmented roads](docs/prefab_roads.ipynb) and [random roads from noise](docs/random_roads.ipynb).
|
||||
203
docs/planar_curve.ipynb
Normal file
203
docs/planar_curve.ipynb
Normal file
File diff suppressed because one or more lines are too long
118
docs/prefab_roads.ipynb
Normal file
118
docs/prefab_roads.ipynb
Normal file
@ -0,0 +1,118 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1a063d05",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Random roads using prefabs\n",
|
||||
"\n",
|
||||
"It is cool to see that a little processed noise can generate something that looks like roads. But of course, roads are not really random. Instead, they are made of a sequence of 'prefabs', for example, a road between A and B may look like\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"\\text{straight, turn left, straight, turn right, straight}\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"Let's see how we can create a random road of length $L$ from a pre-determined set of prefabs.\n",
|
||||
"\n",
|
||||
"#### Random apportionment of total length\n",
|
||||
"\n",
|
||||
"Suppose we want to build a road of length $L$ out of $K$ segments. The total number of waypoints $N$ depends on the step size $\\Delta s$:\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"N = \\frac{L}{\\Delta s}.\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"Let $\\left( p_1, p_2, \\dots, p_K \\right)$ represent the proportion of $N$ that each prefab will be assigned, where $\\sum p_i = 1$. One useful distribution here is the [Dirichlet distribution](https://en.wikipedia.org/wiki/Dirichlet_distribution), which is parametrized by a vector $\\mathbf{\\alpha} = \\left(\\alpha_1, \\alpha_2, \\dots, \\alpha_K \\right)$. The special case where all $\\alpha_i$, the scalar parameter $\\alpha$ is called a *concentration parameter*. Setting the same $\\alpha$ across the entire parameter space makes the distribution symmetric, meaning no prior assumptions are made regarding the proportion of $N$ that will be assigned to each segment. $\\alpha = 1$ leads to what is known as a flat Dirichlet distribution, whereas higher values lead to more dense and evenly distributed $\\left( p_1, p_2, \\dots, p_K \\right)$. On the other hand, keeping $\\alpha \\leq 1$ gives a sparser distribution which can lead to larger variance in apportioned number of waypoints to $\\left( p_1, p_2, \\dots, p_K \\right)$.\n",
|
||||
"\n",
|
||||
"#### Expectation value and variance of Dirichlet distribution\n",
|
||||
"\n",
|
||||
"Suppose we draw our samples for proportion of length from the Dirichlet distribution\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"(p_1, p_2, \\ldots, p_K) \\sim \\text{Dirichlet}(\\alpha, \\alpha, \\ldots, \\alpha)\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"with $\\alpha _{0}=\\sum _{i=1}^{K}\\alpha _{i}$, the mean and variance are then\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"\\operatorname {E} [p_{i}]={\\frac {\\alpha _{i}}{\\alpha _{0}}}, \\; \\operatorname {Var} [p_{i}]={\\frac {\\alpha _{i}(\\alpha _{0}-\\alpha _{i})}{\\alpha _{0}^{2}(\\alpha _{0}+1)}}.\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"If $\\alpha$ is a scalar, then $\\alpha _{0}= K \\alpha$ and the above simplifies to\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"\\operatorname {E} [p_{i}]={\\frac {\\alpha}{K \\alpha}}={\\frac {1}{K}}, \\; \\operatorname {Var} [p_{i}]={\\frac {\\alpha(K \\alpha -\\alpha)}{(K \\alpha)^{2}(K \\alpha +1)}}.\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"We see that $\\operatorname {Var} [p_{i}] \\propto \\frac{1}{\\alpha}$ meaning that the variance reduces with increasing $\\alpha$. We can simply scale the proportions\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"(N \\cdot p_1, N \\cdot p_2, \\ldots, N \\cdot p_K)\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"to get the randomly assigned number of waypoints for each prefab. We now have a distribution which can give randomly assigned lengths to a given list of prefabs, with a parameter to control the degree of randomness. With a large concentration parameter $\\alpha$, the distribution of lengths will be more uniform, with each prefab getting $N \\cdot \\operatorname {E} [p_{i}]={\\frac {N}{K}}$ waypoints assigned to it. Likewise, keeping $\\alpha$ low increases variance and allows for a more random assignment of proportions of waypoints to each prefab segment.\n",
|
||||
"\n",
|
||||
"#### Random angles\n",
|
||||
"\n",
|
||||
"Suppose a turn of a pre-defined arc length $l$ made of $N/K$ waypoints. If one wants to create a random angle, one has to keep in mind that the minimum radius $R_{min}$ depends on the speed of the vehicle and the weather conditions:\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"R_{\\text{min,vehicle}} = \\frac{v^2}{g\\mu},\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"where\n",
|
||||
"- $v$ is the velocity of the vehicle in $\\text{m/s}$,\n",
|
||||
"- $g$ is the gravitational acceleration (about $9.8$ $\\text{m/s}^{2}$), and\n",
|
||||
"- $\\mu$ is the friction coefficient (about $0.7$ for dry asphalt).\n",
|
||||
"\n",
|
||||
"A regular turn (not a U-turn or roundabout) should also have an lower and upper limit on the angle, say, 30 degrees to 90 degrees for a conservative estimate. In terms of radii, it becomes\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"R_{\\text{min}} = \\max\\left(R_{\\text{min,vehicle}}, \\frac{l}{\\pi/2}\\right)\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"and\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"R_{\\text{max}} = \\frac{l}{\\pi/6}.\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"We then sample\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"R \\sim \\text{Uniform}\\left(R_{\\text{min}}, R_{\\text{max\\_angle}}\\right)\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"and obtain a random radius for a turn of arc length $l$ with limits to ensure the radius is large enough given the velocity of the vehicle. Finally, the curvature profile is related to the radius by\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"\\kappa = \\frac{1}{R}\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"which means that the curvature profile of a turn is simply a vector $\\mathbf{\\kappa} = (1/R, \\dots, 1/R)$ with a length of $N/K$ waypoints."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
266
docs/random_roads.ipynb
Normal file
266
docs/random_roads.ipynb
Normal file
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "road_gen"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = [
|
||||
{ name="Pim Nelissen", email="pi0274ne-s@student.lu.se" },
|
||||
]
|
||||
|
||||
@ -33,27 +33,26 @@ 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.
|
||||
|
||||
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:
|
||||
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()}")
|
||||
existing_prefabs = prefabs.PREFABS.keys()
|
||||
if not all(segment in existing_prefabs for segment in segments):
|
||||
raise ValueError(f"Invalid segment type provided. Available choices: {existing_prefabs}")
|
||||
|
||||
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)
|
||||
|
||||
@ -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,9 +29,13 @@ 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.")
|
||||
@ -50,15 +53,23 @@ def main():
|
||||
if args.seed:
|
||||
init_args["seed"] = args.seed
|
||||
|
||||
generator = RandomRoadGenerator(**init_args)
|
||||
|
||||
generate_args = {}
|
||||
if args.method == "random":
|
||||
generator = RandomRoadGenerator(**init_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)
|
||||
|
||||
elif args.method == "segments":
|
||||
generator = SegmentedRoadGenerator(**init_args)
|
||||
|
||||
if args.alpha:
|
||||
generate_args["alpha"] = args.alpha
|
||||
|
||||
generate_args["segments"] = list(args.segments)
|
||||
|
||||
x, y = generator.generate(**generate_args)
|
||||
|
||||
if args.save:
|
||||
|
||||
@ -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
|
||||
|
||||
def test_random_road_generator(test_params):
|
||||
return segments, alpha
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user