mirror of
https://github.com/pim-n/road-gen.git
synced 2026-02-03 09:23:09 +01:00
Initial commit
This commit is contained in:
207
.gitignore
vendored
Normal file
207
.gitignore
vendored
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[codz]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
#poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||||
|
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||||
|
#pdm.lock
|
||||||
|
#pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||||
|
#pixi.lock
|
||||||
|
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||||
|
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||||
|
.pixi
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.envrc
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Abstra
|
||||||
|
# Abstra is an AI-powered process automation framework.
|
||||||
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||||
|
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||||
|
# refer to https://docs.cursor.com/context/ignore-files
|
||||||
|
.cursorignore
|
||||||
|
.cursorindexingignore
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Pim Nelissen
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# RoadGen
|
||||||
|
|
||||||
|
Generate road segments in a 2D Cartesian plane.
|
||||||
29
pyproject.toml
Normal file
29
pyproject.toml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[project]
|
||||||
|
name = "road_gen"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
{ name="Pim Nelissen", email="pi0274ne-s@student.lu.se" },
|
||||||
|
]
|
||||||
|
description = "RoadGen"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"matplotlib>=3.9.2",
|
||||||
|
"numpy>=2"
|
||||||
|
]
|
||||||
|
license = "MIT"
|
||||||
|
license-files = ["LICEN[CS]E*"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=64.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
road-gen = "road_gen.main:main"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/pim-n/road-gen"
|
||||||
|
Issues = "https://github.com/pim-n/road-gen/issues"
|
||||||
0
src/road_gen/__init__.py
Normal file
0
src/road_gen/__init__.py
Normal file
0
src/road_gen/generators/__init__.py
Normal file
0
src/road_gen/generators/__init__.py
Normal file
50
src/road_gen/generators/base_road_generator.py
Normal file
50
src/road_gen/generators/base_road_generator.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import numpy as np
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
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. Default is a random seed.
|
||||||
|
"""
|
||||||
|
if seed == 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
|
||||||
70
src/road_gen/generators/random_road_generator.py
Normal file
70
src/road_gen/generators/random_road_generator.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from typing import Tuple
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .base_road_generator import BaseRoadGenerator
|
||||||
|
from ..integrator.integrator import integrate_road
|
||||||
|
|
||||||
|
class RandomRoadGenerator(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 RandomRoadGenerator 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. Default is a random seed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(length, ds, velocity, mu, g, seed)
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
straight_section_prob: float = 0.05,
|
||||||
|
straight_section_max_rel_size: float = 0.1
|
||||||
|
) -> Tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""Generate a random road according to specified parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
straight_section_prob (float, optional): Probability at every step i that a straight section will start. Defaults to 0.05.
|
||||||
|
straight_section_max_rel_size (float, optional): The maximum size that straight section(s) can have relative to the total length of the path. Defaults to 0.1.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[np.ndarray, np.ndarray]: x and y coordinates of the waypoints describing the random road.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.straight_section_prob = straight_section_prob
|
||||||
|
self.straight_section_max_rel_size = straight_section_max_rel_size
|
||||||
|
|
||||||
|
num_points = int(self.length / (self.ds))
|
||||||
|
max_curvature = 1 / self.min_radius
|
||||||
|
|
||||||
|
white_noise = self._rng.standard_normal(num_points)
|
||||||
|
curvature = np.clip(white_noise, -max_curvature, max_curvature)
|
||||||
|
|
||||||
|
# Randomly add multiple straight sections
|
||||||
|
i = 0
|
||||||
|
while i < num_points:
|
||||||
|
if np.random.rand() < straight_section_prob:
|
||||||
|
# Random straight segment length
|
||||||
|
max_len = int(num_points * straight_section_max_rel_size)
|
||||||
|
seg_len = np.random.randint(1, max_len + 1)
|
||||||
|
curvature[i:i+seg_len] = 0 # set curvature to zero for straight
|
||||||
|
i += seg_len # skip over straight segment
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
x, y = integrate_road(curvature)
|
||||||
|
|
||||||
|
return x * self.ds, y * self.ds
|
||||||
|
|
||||||
99
src/road_gen/generators/segmented_road_generator.py
Normal file
99
src/road_gen/generators/segmented_road_generator.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
from .base_road_generator import BaseRoadGenerator
|
||||||
|
from ..prefabs import prefabs
|
||||||
|
from ..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. Default is a random seed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(length, ds, velocity, mu, g, seed)
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
segments: list[str],
|
||||||
|
alpha: float = 1.0
|
||||||
|
) -> 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_
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: _description_
|
||||||
|
"""
|
||||||
|
if not all(segment in prefabs.PREFABS.keys() for segment in segments):
|
||||||
|
raise ValueError(f"Invalid segment type provided. Available choices: {prefabs.SEGMENTS.keys()}")
|
||||||
|
|
||||||
|
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.pi / 2)
|
||||||
|
R_max_angle = seg_length / (np.pi / 6)
|
||||||
|
|
||||||
|
# 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 = np.random.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
|
||||||
0
src/road_gen/integrator/__init__.py
Normal file
0
src/road_gen/integrator/__init__.py
Normal file
21
src/road_gen/integrator/integrator.py
Normal file
21
src/road_gen/integrator/integrator.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def integrate_road(curvature: np.ndarray, ds: float = 1.0):
|
||||||
|
"""Integrate along the curvature field to obtain X and Y coordinates of the road.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
curvature (np.ndarray): The curvature field.
|
||||||
|
ds (float, optional): _description_. Defaults to 1.0.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_type_: _description_
|
||||||
|
"""
|
||||||
|
|
||||||
|
theta = np.zeros(len(curvature))
|
||||||
|
theta[1:] = np.cumsum(curvature[:-1] * ds)
|
||||||
|
|
||||||
|
x = np.cumsum(np.cos(theta) * ds)
|
||||||
|
y = np.cumsum(np.sin(theta) * ds)
|
||||||
|
|
||||||
|
return x, y
|
||||||
77
src/road_gen/main.py
Normal file
77
src/road_gen/main.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import argparse
|
||||||
|
|
||||||
|
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 .utils import export
|
||||||
|
|
||||||
|
def add_common_args(parser):
|
||||||
|
"""Add common arguments to a subparser."""
|
||||||
|
parser.add_argument("--length", "-l", type=int, required=True, help="Length of road in meters.")
|
||||||
|
parser.add_argument("--ds", "-s", type=int, required=True, help="Step size in meters.")
|
||||||
|
parser.add_argument("--velocity", "-v", type=float, required=True, help="Velocity in meters per second. Needed to determine the minimum radius of turns.")
|
||||||
|
|
||||||
|
parser.add_argument("--mu", type=float, required=False, help="Friction coefficient. Defaults to 0.7, which represents dry asphalt.")
|
||||||
|
parser.add_argument("--g", type=float, required=False, help="Acceleration due to gravitation. Defaults to 9.81 meters per second.")
|
||||||
|
|
||||||
|
parser.add_argument("--seed", type=int, required=False, help="Fix a seed for reproducibility")
|
||||||
|
parser.add_argument("--save", action="store_true", required=False, help="Save all outputs. If false, just plots.")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Generate stuff with two methods.")
|
||||||
|
subparsers = parser.add_subparsers(dest="method", required=True)
|
||||||
|
|
||||||
|
random_parser = subparsers.add_parser("random", help="Generate a random road according to parameters.")
|
||||||
|
random_parser.add_argument("--straight_section_prob", type=float, required=False, help="Probability at every step i that a straight section will start. Defaults to 0.05.")
|
||||||
|
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)
|
||||||
|
|
||||||
|
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.")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
44
src/road_gen/plotting/plot_road.py
Normal file
44
src/road_gen/plotting/plot_road.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
from ..generators.random_road_generator import RandomRoadGenerator
|
||||||
|
from ..generators.segmented_road_generator import SegmentedRoadGenerator
|
||||||
|
|
||||||
|
def plot_road(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
generator: RandomRoadGenerator | SegmentedRoadGenerator,
|
||||||
|
save: bool = False,
|
||||||
|
filename: str = "out.jpg"
|
||||||
|
):
|
||||||
|
|
||||||
|
plt.plot(x, y, '-b')
|
||||||
|
plt.xlabel("X (m)")
|
||||||
|
plt.ylabel("Y (m)")
|
||||||
|
|
||||||
|
title_str = (
|
||||||
|
f"$L = {generator.length}$ m, " +
|
||||||
|
f"$v = {generator.velocity}$ m/s, " +
|
||||||
|
f"$\\Delta s = {generator.ds} m$"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(generator, RandomRoadGenerator):
|
||||||
|
title_str += (
|
||||||
|
"\n" +
|
||||||
|
f"Prob(straight) $= {generator.straight_section_prob}$, " +
|
||||||
|
f"Max rel. size $= {generator.straight_section_max_rel_size}$"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
title_str += (
|
||||||
|
f"$, \\alpha = {generator.alpha}$" +
|
||||||
|
"\n" +
|
||||||
|
str(generator.segments)
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.title(title_str)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
if save:
|
||||||
|
plt.savefig(filename)
|
||||||
|
else:
|
||||||
|
plt.show()
|
||||||
0
src/road_gen/prefabs/__init__.py
Normal file
0
src/road_gen/prefabs/__init__.py
Normal file
19
src/road_gen/prefabs/prefabs.py
Normal file
19
src/road_gen/prefabs/prefabs.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_available_prefabs():
|
||||||
|
print(PREFABS.keys())
|
||||||
0
src/road_gen/utils/__init__.py
Normal file
0
src/road_gen/utils/__init__.py
Normal file
49
src/road_gen/utils/export.py
Normal file
49
src/road_gen/utils/export.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from ..generators.random_road_generator import RandomRoadGenerator
|
||||||
|
from ..generators.segmented_road_generator import SegmentedRoadGenerator
|
||||||
|
|
||||||
|
def coords_to_json(
|
||||||
|
x: numpy.ndarray,
|
||||||
|
y: numpy.ndarray,
|
||||||
|
filename: str = "output.json",
|
||||||
|
x_key: str = "x",
|
||||||
|
y_key: str = "y"
|
||||||
|
) -> str:
|
||||||
|
"""Convert x and y arrays of waypoints to a JSON.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (numpy.ndarray): x coordinates.
|
||||||
|
y (numpy.ndarray): y coordinates.
|
||||||
|
x_key (str, optional): Key for x coordinates. Defaults to "x".
|
||||||
|
y_key (str, optional): Key for y coordinates. Defaults to "y".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON string containing the data.
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
x_key: x.tolist(), # Convert numpy array to list
|
||||||
|
y_key: y.tolist()
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(filename, "w") as file:
|
||||||
|
json.dump(data, file, indent=4)
|
||||||
|
|
||||||
|
def params_to_json(
|
||||||
|
generator: RandomRoadGenerator | SegmentedRoadGenerator,
|
||||||
|
filename: str = "output.json"
|
||||||
|
):
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
for attr in dir(generator):
|
||||||
|
# skip private and special attributes
|
||||||
|
if not attr.startswith('_'):
|
||||||
|
value = getattr(generator, attr)
|
||||||
|
if not callable(value):
|
||||||
|
params[attr] = value
|
||||||
|
|
||||||
|
with open(filename, "w") as file:
|
||||||
|
json.dump(params, file, indent=4)
|
||||||
Reference in New Issue
Block a user